Added dialog system.

Started to implement recon dialog.
Added long-running recon process, which doesn't actually do reconciliation just yet.

git-svn-id: http://google-refine.googlecode.com/svn/trunk@17 7d457c2a-affb-35e4-300a-418c747d4874
This commit is contained in:
David Huynh 2010-02-01 08:31:50 +00:00
parent 06b5373151
commit 17cbe6b62d
18 changed files with 487 additions and 41 deletions

View File

@ -23,6 +23,7 @@ import com.metaweb.gridlock.commands.GetHistoryCommand;
import com.metaweb.gridlock.commands.GetProcessesCommand;
import com.metaweb.gridlock.commands.GetProjectMetadataCommand;
import com.metaweb.gridlock.commands.GetRowsCommand;
import com.metaweb.gridlock.commands.ReconcileCommand;
import com.metaweb.gridlock.commands.UndoRedoCommand;
public class GridlockServlet extends HttpServlet {
@ -42,6 +43,7 @@ public class GridlockServlet extends HttpServlet {
_commands.put("undo-redo", new UndoRedoCommand());
_commands.put("compute-facets", new ComputeFacetsCommand());
_commands.put("do-text-transform", new DoTextTransformCommand());
_commands.put("reconcile", new ReconcileCommand());
}
@Override

View File

@ -10,6 +10,9 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.metaweb.gridlock.browsing.Engine;
import com.metaweb.gridlock.browsing.FilteredRows;
import com.metaweb.gridlock.browsing.RowVisitor;
import com.metaweb.gridlock.expr.Evaluable;
import com.metaweb.gridlock.expr.Parser;
import com.metaweb.gridlock.history.CellChange;
@ -27,45 +30,60 @@ public class DoTextTransformCommand extends Command {
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
Project project = getProject(request);
int cellIndex = Integer.parseInt(request.getParameter("cell"));
Column column = project.columnModel.getColumnByCellIndex(cellIndex);
if (column == null) {
respond(response, "{ \"code\" : \"error\", \"message\" : \"No such column\" }");
return;
}
String columnName = column.headerLabel;
String expression = request.getParameter("expression");
try {
Project project = getProject(request);
Engine engine = getEngine(request, project);
int cellIndex = Integer.parseInt(request.getParameter("cell"));
Column column = project.columnModel.getColumnByCellIndex(cellIndex);
if (column == null) {
respond(response, "{ \"code\" : \"error\", \"message\" : \"No such column\" }");
return;
}
String columnName = column.headerLabel;
String expression = request.getParameter("expression");
Evaluable eval = new Parser(expression).getExpression();
//System.out.println("--- " + eval.toString());
Properties bindings = new Properties();
bindings.put("project", project);
List<CellChange> cellChanges = new ArrayList<CellChange>(project.rows.size());
for (int r = 0; r < project.rows.size(); r++) {
Row row = project.rows.get(r);
if (cellIndex < row.cells.size()) {
Cell cell = row.cells.get(cellIndex);
if (cell.value == null) {
continue;
}
FilteredRows filteredRows = engine.getAllFilteredRows();
filteredRows.accept(project, new RowVisitor() {
int cellIndex;
Properties bindings;
List<CellChange> cellChanges;
Evaluable eval;
bindings.put("cell", cell);
bindings.put("value", cell.value);
Cell newCell = new Cell();
newCell.value = eval.evaluate(bindings);
newCell.recon = cell.recon;
CellChange cellChange = new CellChange(r, cellIndex, cell, newCell);
cellChanges.add(cellChange);
public RowVisitor init(int cellIndex, Properties bindings, List<CellChange> cellChanges, Evaluable eval) {
this.cellIndex = cellIndex;
this.bindings = bindings;
this.cellChanges = cellChanges;
this.eval = eval;
return this;
}
}
@Override
public boolean visit(Project project, int rowIndex, Row row) {
if (cellIndex < row.cells.size()) {
Cell cell = row.cells.get(cellIndex);
if (cell.value != null) {
bindings.put("cell", cell);
bindings.put("value", cell.value);
Cell newCell = new Cell();
newCell.value = eval.evaluate(bindings);
newCell.recon = cell.recon;
CellChange cellChange = new CellChange(rowIndex, cellIndex, cell, newCell);
cellChanges.add(cellChange);
}
}
return false;
}
}.init(cellIndex, bindings, cellChanges, eval));
MassCellChange massCellChange = new MassCellChange(cellChanges);
HistoryEntry historyEntry = new HistoryEntry(

View File

@ -0,0 +1,82 @@
package com.metaweb.gridlock.commands;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.metaweb.gridlock.browsing.Engine;
import com.metaweb.gridlock.browsing.FilteredRows;
import com.metaweb.gridlock.browsing.RowVisitor;
import com.metaweb.gridlock.model.Cell;
import com.metaweb.gridlock.model.Column;
import com.metaweb.gridlock.model.Project;
import com.metaweb.gridlock.model.Row;
import com.metaweb.gridlock.process.ReconProcess;
import com.metaweb.gridlock.process.ReconProcess.ReconEntry;
public class ReconcileCommand extends Command {
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
try {
Project project = getProject(request);
Engine engine = getEngine(request, project);
int cellIndex = Integer.parseInt(request.getParameter("cell"));
Column column = project.columnModel.getColumnByCellIndex(cellIndex);
if (column == null) {
respond(response, "{ \"code\" : \"error\", \"message\" : \"No such column\" }");
return;
}
String columnName = column.headerLabel;
String typeID = request.getParameter("type");
List<ReconEntry> entries = new ArrayList<ReconEntry>(project.rows.size());
FilteredRows filteredRows = engine.getAllFilteredRows();
filteredRows.accept(project, new RowVisitor() {
int cellIndex;
List<ReconEntry> entries;
public RowVisitor init(int cellIndex, List<ReconEntry> entries) {
this.cellIndex = cellIndex;
this.entries = entries;
return this;
}
@Override
public boolean visit(Project project, int rowIndex, Row row) {
if (cellIndex < row.cells.size()) {
Cell cell = row.cells.get(cellIndex);
if (cell.value != null) {
entries.add(new ReconEntry(rowIndex, cell));
}
}
return false;
}
}.init(cellIndex, entries));
ReconProcess process = new ReconProcess(
project,
"Reconcile " + columnName + " to type " + typeID,
cellIndex,
entries,
typeID
);
boolean done = project.processManager.queueProcess(process);
respond(response, "{ \"code\" : " + (done ? "\"ok\"" : "\"pending\"") + " }");
} catch (Exception e) {
respondException(response, e);
}
}
}

View File

@ -25,7 +25,11 @@ public class History implements Serializable {
}
public void addEntry(HistoryEntry entry) {
for (HistoryEntry entry2 : _futureEntries) {
entry2.delete();
}
_futureEntries.clear();
_pastEntries.add(entry);
entry.apply(ProjectManager.singleton.getProject(_projectID));
}
@ -54,6 +58,21 @@ public class History implements Serializable {
}
}
protected HistoryEntry getEntry(long entryID) {
for (int i = 0; i < _pastEntries.size(); i++) {
if (_pastEntries.get(i).id == entryID) {
return _pastEntries.get(i);
}
}
for (int i = 0; i < _futureEntries.size(); i++) {
if (_futureEntries.get(i).id == entryID) {
return _futureEntries.get(i);
}
}
return null;
}
protected void undo(int times) {
Project project = ProjectManager.singleton.getProject(_projectID);

View File

@ -64,6 +64,13 @@ public class HistoryEntry implements Serializable {
_change.revert(project);
}
public void delete() {
File file = getFile();
if (file.exists()) {
file.delete();
}
}
protected void loadChange() {
File file = getFile();

View File

@ -1,20 +1,35 @@
package com.metaweb.gridlock.history;
import java.util.Properties;
import org.json.JSONException;
import org.json.JSONObject;
import com.metaweb.gridlock.model.Project;
import com.metaweb.gridlock.process.Process;
import com.metaweb.gridlock.process.ProcessManager;
public class HistoryProcess extends Process {
final protected Project _project;
final protected long _lastDoneID;
final protected long _lastDoneID;
final protected String _description;
boolean _done = false;
public HistoryProcess(Project project, long lastDoneID) {
_project = project;
_lastDoneID = lastDoneID;
if (_lastDoneID == 0) {
_description = "Undo all";
} else {
HistoryEntry entry = _project.history.getEntry(_lastDoneID);
_description = "Undo/redo until after " + entry.description;
}
}
@Override
public void cancel() {
throw new RuntimeException("Not a long-running process");
}
@Override
@ -25,9 +40,32 @@ public class HistoryProcess extends Process {
@Override
public void performImmediate() {
_project.history.undoRedo(_lastDoneID);
_done = true;
}
@Override
public void startPerforming(ProcessManager manager) {
throw new RuntimeException("Not a long-running process");
}
@Override
public JSONObject getJSON(Properties options) throws JSONException {
JSONObject o = new JSONObject();
o.put("description", _description);
o.put("immediate", true);
o.put("status", _done ? "done" : "pending");
return o;
}
@Override
public boolean isDone() {
throw new RuntimeException("Not a long-running process");
}
@Override
public boolean isRunning() {
throw new RuntimeException("Not a long-running process");
}
}

View File

@ -6,10 +6,11 @@ import org.json.JSONException;
import org.json.JSONObject;
abstract public class LongRunningProcess extends Process {
final protected String _description;
final protected String _description;
protected ProcessManager _manager;
protected Thread _thread;
protected int _progress; // out of 100
protected boolean _canceled;
protected LongRunningProcess(String description) {
_description = description;
@ -17,11 +18,14 @@ abstract public class LongRunningProcess extends Process {
@Override
public void cancel() {
// TODO Auto-generated method stub
_canceled = true;
if (_thread != null && _thread.isAlive()) {
_thread.interrupt();
}
}
@Override
public
JSONObject getJSON(Properties options) throws JSONException {
JSONObject o = new JSONObject();
@ -38,6 +42,16 @@ abstract public class LongRunningProcess extends Process {
return false;
}
@Override
public boolean isRunning() {
return _thread != null && _thread.isAlive();
}
@Override
public boolean isDone() {
return _thread != null && !_thread.isAlive();
}
@Override
public void performImmediate() {
throw new RuntimeException("Not an immediate process");

View File

@ -16,5 +16,5 @@ public abstract class Process {
abstract public void startPerforming(ProcessManager manager);
abstract public void cancel();
abstract JSONObject getJSON(Properties options) throws JSONException;
public abstract JSONObject getJSON(Properties options) throws JSONException;
}

View File

@ -33,11 +33,15 @@ public class ProcessManager {
return true;
} else {
_processes.add(process);
update();
return false;
}
}
public void onDoneProcess(Process p) {
_processes.remove(p);
update();
}

View File

@ -45,6 +45,7 @@ public class QuickHistoryEntryProcess extends Process {
}
@Override
public
JSONObject getJSON(Properties options) throws JSONException {
JSONObject o = new JSONObject();

View File

@ -0,0 +1,73 @@
package com.metaweb.gridlock.process;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import com.metaweb.gridlock.model.Cell;
import com.metaweb.gridlock.model.Project;
public class ReconProcess extends LongRunningProcess implements Runnable {
static public class ReconEntry {
final public int rowIndex;
final public Cell cell;
public ReconEntry(int rowIndex, Cell cell) {
this.rowIndex = rowIndex;
this.cell = cell;
}
}
final protected Project _project;
final protected int _cellIndex;
final protected List<ReconEntry> _entries;
final protected String _typeID;
public ReconProcess(Project project, String description, int cellIndex, List<ReconEntry> entries, String typeID) {
super(description);
_project = project;
_cellIndex = cellIndex;
_entries = entries;
_typeID = typeID;
}
@Override
protected Runnable getRunnable() {
return this;
}
@Override
public void run() {
Map<String, List<ReconEntry>> valueToEntries = new HashMap<String, List<ReconEntry>>();
for (ReconEntry entry : _entries) {
Object value = entry.cell.value;
if (value != null && value instanceof String) {
List<ReconEntry> entries2;
if (valueToEntries.containsKey(value)) {
entries2 = valueToEntries.get(value);
} else {
entries2 = new LinkedList<ReconEntry>();
valueToEntries.put((String) value, entries2);
}
entries2.add(entry);
}
}
List<String> values = new ArrayList<String>(valueToEntries.keySet());
for (int i = 0; i < values.size(); i += 20) {
_progress = i * 100 / values.size();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
if (_canceled) {
break;
}
}
}
_project.processManager.onDoneProcess(this);
}
}

View File

@ -1 +1 @@
<html> <head> <title>Gridlock</title> <link rel="stylesheet" href="/styles/common.css" /> <link rel="stylesheet" href="/styles/project.css" /> <link rel="stylesheet" href="/styles/history.css" /> <link rel="stylesheet" href="/styles/browsing.css" /> <link rel="stylesheet" href="/styles/process.css" /> <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js"></script> <script type="text/javascript" src="scripts/util/misc.js"></script> <script type="text/javascript" src="scripts/util/url.js"></script> <script type="text/javascript" src="scripts/util/string.js"></script> <script type="text/javascript" src="scripts/util/ajax.js"></script> <script type="text/javascript" src="scripts/util/menu.js"></script> <script type="text/javascript" src="scripts/project.js"></script> <script type="text/javascript" src="scripts/project/list-facet.js"></script> <script type="text/javascript" src="scripts/project/browsing-engine.js"></script> <script type="text/javascript" src="scripts/project/data-table-view.js"></script> <script type="text/javascript" src="scripts/project/history-widget.js"></script> <script type="text/javascript" src="scripts/project/process-widget.js"></script> </head> <body> <div id="header"> <h1 id="title">Gridlock</h1> </div> <div id="body"> Loading ... </div> </body> </html>
<html> <head> <title>Gridlock</title> <link rel="stylesheet" href="/styles/common.css" /> <script type="text/javascript" src="scripts/util/url.js"></script> <link rel="stylesheet" href="/styles/common.css" /> <link rel="stylesheet" href="/styles/project.css" /> <link rel="stylesheet" href="/styles/history.css" /> <link rel="stylesheet" href="/styles/browsing.css" /> <link rel="stylesheet" href="/styles/process.css" /> <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js"></script> <script type="text/javascript" src="http://freebaselibs.com/static/suggest/1.0.3/suggest.min.js"></script> <script type="text/javascript" src="scripts/util/misc.js"></script> <script type="text/javascript" src="scripts/util/url.js"></script> <script type="text/javascript" src="scripts/util/string.js"></script> <script type="text/javascript" src="scripts/util/ajax.js"></script> <script type="text/javascript" src="scripts/util/menu.js"></script> <script type="text/javascript" src="scripts/util/dialog.js"></script> <script type="text/javascript" src="scripts/project.js"></script> <script type="text/javascript" src="scripts/project/list-facet.js"></script> <script type="text/javascript" src="scripts/project/browsing-engine.js"></script> <script type="text/javascript" src="scripts/project/data-table-view.js"></script> <script type="text/javascript" src="scripts/project/history-widget.js"></script> <script type="text/javascript" src="scripts/project/process-widget.js"></script> <script type="text/javascript" src="scripts/project/recon-dialog.js"></script> </head> <body> <div id="header"> <h1 id="title">Gridlock</h1> </div> <div id="body"> Loading ... </div> </body> </html>

View File

@ -45,6 +45,7 @@ DataTableView.prototype.render = function() {
var trHead = table.insertRow(0);
var td = trHead.insertCell(trHead.cells.length);
$(td).addClass("column-header");
$('<img src="/images/menu-dropdown.png" />').addClass("column-header-menu").appendTo(td).click(function() {
self._createMenuForAllColumns(this);
});
@ -267,7 +268,9 @@ DataTableView.prototype._createMenuForColumnHeader = function(column, index, elm
submenu: [
{
label: "Initiate on Filtered Rows...",
click: function() {}
click: function() {
new ReconDialog(index);
}
},
{},
{
@ -283,7 +286,7 @@ DataTableView.prototype._doTextTransform = function(column, expression) {
var self = this;
$.post(
"/command/do-text-transform?" + $.param({ project: theProject.id, cell: column.cellIndex, expression: expression }),
null,
{ engine: JSON.stringify(ui.browsingEngine.getJSON()) },
function(data) {
if (data.code == "ok") {
self.update();

View File

@ -24,9 +24,11 @@ ProcessWidget.prototype._render = function() {
this._div.empty();
var bodyDiv = $('<div></div>').addClass("process-panel-inner").text("Testing").appendTo(this._div);
var bodyDiv = $('<div></div>').addClass("process-panel-inner").appendTo(this._div);
if (this._data.processes.length == 0) {
this._div.hide();
ui.historyWidget.update();
return;
}

View File

@ -0,0 +1,48 @@
function ReconDialog(columnIndex) {
this._columnIndex = columnIndex;
this._column = theProject.columnModel.columns[columnIndex];
this._createDialog();
}
ReconDialog.prototype._createDialog = function() {
var self = this;
var frame = DialogSystem.createDialog();
frame.width("400px");
var header = $('<div></div>').addClass("dialog-header").text("Reconcile column " + this._column.headerLabel).appendTo(frame);
var body = $('<div></div>').addClass("dialog-body").appendTo(frame);
var footer = $('<div></div>').addClass("dialog-footer").appendTo(frame);
$('<p></p>').text("Reconcile cell values to topics of type:").appendTo(body);
var type = null;
var input = $('<input />').appendTo($('<p></p>').appendTo(body));
input.suggest({ type : '/type/type' }).bind("fb-select", function(e, data) {
type = data.id;
});
$('<button></button>').text("Start Reconciling").click(function() {
DialogSystem.dismissUntil(level - 1);
$.post(
"/command/reconcile?" + $.param({ project: theProject.id, cell: self._column.cellIndex, type: type }),
{ engine: JSON.stringify(ui.browsingEngine.getJSON()) },
function(data) {
if (data.code != "error") {
ui.processWidget.update();
} else {
alert(data.message);
}
},
"json"
);
}).appendTo(footer);
$('<button></button>').text("Cancel").click(function() {
DialogSystem.dismissUntil(level - 1);
}).appendTo(footer);
var level = DialogSystem.showDialog(frame);
input[0].focus();
};

View File

@ -0,0 +1,70 @@
DialogSystem = {
_layers: [],
_bottomOverlay: null
};
DialogSystem.showDialog = function(elmt, onCancel) {
if (DialogSystem._bottomOverlay == null) {
DialogSystem._bottomOverlay = $('<div>&nbsp;</div>')
.addClass("dialog-overlay")
.css("z-index", 100)
.appendTo(document.body);
}
var overlay = $('<div>&nbsp;</div>')
.addClass("dialog-overlay2")
.css("z-index", 101 + DialogSystem._layers.length * 2)
.appendTo(document.body);
var container = $('<div></div>')
.addClass("dialog-container")
.css("z-index", 102 + DialogSystem._layers.length * 2)
.appendTo(document.body);
elmt.css("visibility", "hidden").appendTo(container);
container.css("top", Math.round((overlay.height() - elmt.height()) / 2) + "px");
elmt.css("visibility", "visible");
var layer = {
overlay: overlay,
container: container,
onCancel: onCancel
};
DialogSystem._layers.push(layer);
var level = DialogSystem._layers.length;
return level;
};
DialogSystem.dismissAll = function() {
DialogSystem.dismissUntil(0);
};
DialogSystem.dismissUntil = function(level) {
for (var i = DialogSystem._layers.length - 1; i >= level; i--) {
var layer = DialogSystem._layers[i];
layer.overlay.remove();
layer.container.remove();
if (layer.onCancel) {
try {
layer.onCancel();
} catch (e) {
console.log(e);
}
}
}
DialogSystem._layers = DialogSystem._layers.slice(0, level);
if (level == 0) {
if (DialogSystem._bottomOverlay != null) {
DialogSystem._bottomOverlay.remove();
DialogSystem._bottomOverlay = null;
}
}
};
DialogSystem.createDialog = function() {
return $('<div></div>').addClass("dialog-frame");
};

View File

@ -89,3 +89,64 @@ a.menu-item:hover {
a.menu-item img {
border: none;
}
.dialog-overlay {
background: black;
opacity: 0.3;
position: fixed;
padding: 0px;
margin: 0px;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
}
.dialog-overlay2 {
position: fixed;
padding: 0px;
margin: 0px;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
background: black;
opacity: 0.01;
}
.dialog-container {
position: fixed;
padding: 0px;
margin: 0px;
left: 0px;
width: 100%;
text-align: center;
}
.dialog-frame {
margin: 0 auto;
text-align: left;
background: white;
border: 1px solid #aaa;
padding: 2px;
}
.dialog-header {
background: #ccc;
padding: 5px 10px;
font-weight: bold;
}
.dialog-body {
padding: 10px;
}
.dialog-footer {
background: #ddd;
padding: 5px 10px;
text-align: right;
}
.dialog-footer button {
margin-left: 5px;
}

View File

@ -13,3 +13,7 @@
margin: 0 auto;
text-align: left;
}
.fbs-pane, .fbs-flyout-pane {
z-index: 200;
}