diff --git a/dev/codeserver/pom.xml b/dev/codeserver/pom.xml index 5de3e7781..88b246b0b 100644 --- a/dev/codeserver/pom.xml +++ b/dev/codeserver/pom.xml @@ -155,6 +155,10 @@ org.apache.solr solr-solrj + + org.apache.solr + solr-solrj-jetty + ch.qos.logback logback-classic diff --git a/src/main/java/com/databasepreservation/common/client/common/visualization/browse/information/DatabaseInformationPanel.java b/src/main/java/com/databasepreservation/common/client/common/visualization/browse/information/DatabaseInformationPanel.java index f2e8c65ea..f6af30d32 100644 --- a/src/main/java/com/databasepreservation/common/client/common/visualization/browse/information/DatabaseInformationPanel.java +++ b/src/main/java/com/databasepreservation/common/client/common/visualization/browse/information/DatabaseInformationPanel.java @@ -161,14 +161,20 @@ private void loadOnAccess() { private void initDataContent() { if (database.getMetadata().getSchemas().size() == 1) { + final DataPanel instance = DataPanel.getInstance(database, database.getMetadata().getSchemas().get(0).getUuid(), + status); + instance.reload(advancedMode); + if (dataContentCard.getWidgetCount() == 1) { FlowPanel cardTitlePanel = CommonClientUtils.getCardTitle(messages.schema()); cardTitlePanel.addStyleName("card-header"); + ViewerSchema schema = database.getMetadata().getSchemas().get(0); + ErDiagram erDiagram = ErDiagram.getInstance(database, schema, + HistoryManager.getCurrentHistoryPath().get(0), status); + cardTitlePanel.add(erDiagram.getLayoutConfigButton()); dataContentCard.insert(cardTitlePanel, 0); } - final DataPanel instance = DataPanel.getInstance(database, database.getMetadata().getSchemas().get(0).getUuid(), - status); - instance.reload(advancedMode); + dataContent.add(instance); } else { dataContentCard.addStyleName("card-diagram"); diff --git a/src/main/java/com/databasepreservation/common/client/common/visualization/browse/information/ErDiagram.java b/src/main/java/com/databasepreservation/common/client/common/visualization/browse/information/ErDiagram.java index ace4a461c..466c92cb0 100644 --- a/src/main/java/com/databasepreservation/common/client/common/visualization/browse/information/ErDiagram.java +++ b/src/main/java/com/databasepreservation/common/client/common/visualization/browse/information/ErDiagram.java @@ -27,13 +27,16 @@ import com.databasepreservation.common.client.widgets.wcag.AccessibleFocusPanel; import com.github.nmorel.gwtjackson.client.ObjectMapper; import com.google.gwt.core.client.GWT; +import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.logical.shared.SelectionEvent; import com.google.gwt.event.logical.shared.SelectionHandler; import com.google.gwt.uibinder.client.UiBinder; import com.google.gwt.uibinder.client.UiField; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HTML; import com.google.gwt.user.client.ui.MultiWordSuggestOracle; +import com.google.gwt.user.client.ui.PopupPanel; import com.google.gwt.user.client.ui.SimplePanel; import com.google.gwt.user.client.ui.SuggestBox; import com.google.gwt.user.client.ui.SuggestOracle; @@ -55,6 +58,8 @@ public class ErDiagram extends Composite implements ICollectionStatusObserver { AccessibleFocusPanel searchButton; @UiField AccessibleFocusPanel clearCrossButton; + private AccessibleFocusPanel layoutConfigButton; + private PopupPanel layoutDropdown; @UiField FlowPanel contentItems; @@ -164,6 +169,55 @@ public void onSelection(SelectionEvent event) { resetDiagramFocus(schema.getUuid()); searchBox.setFocus(true); }); + + layoutConfigButton = new AccessibleFocusPanel(); + layoutConfigButton.addStyleName("btn btn-link btn-pull-right"); + layoutConfigButton.add(new HTML("Configuration ")); + layoutDropdown = buildLayoutDropdown(); + layoutConfigButton.addClickHandler(event -> { + if (layoutDropdown.isShowing()) { + layoutDropdown.hide(); + } else { + layoutDropdown.showRelativeTo(layoutConfigButton); + } + }); + } + + private PopupPanel buildLayoutDropdown() { + PopupPanel popup = new PopupPanel(true); + popup.addStyleName("layoutDropdownPopup"); + + FlowPanel menu = new FlowPanel(); + menu.addStyleName("layoutDropdownMenu"); + menu.add(dropdownItem("fa-upload", "Upload layout", + () -> uploadPositions(databaseUUID, schema.getUuid()))); + menu.add(dropdownItem("fa-download", "Download layout", + () -> downloadPositions(schema.getUuid()))); + menu.add(dropdownItem("fa-undo", "Reset layout", this::resetLayout)); + + popup.setWidget(menu); + return popup; + } + + private FlowPanel dropdownItem(String iconClass, String label, Runnable action) { + FlowPanel item = new FlowPanel(); + item.addStyleName("layoutDropdownItem"); + item.add(new HTML(" " + label)); + item.addDomHandler(event -> { + layoutDropdown.hide(); + action.run(); + }, ClickEvent.getType()); + return item; + } + + public AccessibleFocusPanel getLayoutConfigButton() { + return layoutConfigButton; + } + + private void resetLayout() { + clearSavedPositions(databaseUUID, schema.getUuid()); + isInitialized = false; + renderDiagram(); } /** @@ -729,27 +783,64 @@ public static native void loadDiagram(String dbuuid, String schemaUUID, String n (function erdiagramload(){ // network container var container = $wnd.document.getElementById('erdiagram-' + schemaUUID); - + // Destroy the previous graph instance to avoid overlap if (container && container.network) { container.network.destroy(); container.network = null; } - + // create an array with nodes var rawNodes = eval(nodesJson); - + var rawNodesLen = rawNodes.length; for(var i=0; i 1){ options.physics.enabled = false; network.setOptions(options); + + // When some nodes had saved positions and others did not, physics settled + // the new nodes around the fixed ones — now unfix all and persist the full layout. + if (savedPositions && !allPositioned) { + var nodeIds = network.body.data.nodes.getIds(); + var updates = []; + for (var ui = 0; ui < nodeIds.length; ui++) { + updates.push({id: nodeIds[ui], fixed: {x: false, y: false}}); + } + network.body.data.nodes.update(updates); + savePositions(); + } } - // network.fit(); }); - + network.on("dragEnd", function (params) { + // Save positions only after physics is off and the user actually moved a node + if (!options.physics.enabled && params.nodes && params.nodes.length > 0) { + savePositions(); + } network.unselectAll(); }); })(); @@ -935,4 +1045,69 @@ private native void resetDiagramFocus(String schemaUUID) /*-{ }); } }-*/; + + private static native void uploadPositions(String dbuuid, String schemaUUID) /*-{ + var storageKey = 'erdiagram-positions-' + dbuuid + '-' + schemaUUID; + var container = $wnd.document.getElementById('erdiagram-' + schemaUUID); + + var input = $wnd.document.createElement('input'); + input.type = 'file'; + input.accept = '.json,application/json'; + input.style.display = 'none'; + + input.onchange = function(event) { + var file = event.target.files[0]; + if (!file) return; + var reader = new $wnd.FileReader(); + reader.onload = function(e) { + try { + var positions = JSON.parse(e.target.result); + $wnd.localStorage.setItem(storageKey, JSON.stringify(positions)); + + if (container && container.network) { + var network = container.network; + var updates = []; + for (var nodeId in positions) { + if (positions.hasOwnProperty(nodeId)) { + updates.push({id: nodeId, x: positions[nodeId].x, y: positions[nodeId].y, fixed: {x: false, y: false}}); + } + } + network.body.data.nodes.update(updates); + network.setOptions({physics: {enabled: false}}); + network.fit({animation: {duration: 500, easingFunction: "easeInOutQuad"}}); + } + } catch(err) { + $wnd.alert('Invalid layout file: ' + err.message); + } + if (input.parentNode) input.parentNode.removeChild(input); + }; + reader.readAsText(file); + }; + + $wnd.document.body.appendChild(input); + input.click(); + }-*/; + + private static native void downloadPositions(String schemaUUID) /*-{ + var container = $wnd.document.getElementById('erdiagram-' + schemaUUID); + if (!container || !container.network) return; + var positions = container.network.getPositions(); + var json = JSON.stringify(positions, null, 2); + var blob = new Blob([json], {type: 'application/json'}); + var url = $wnd.URL.createObjectURL(blob); + var a = $wnd.document.createElement('a'); + a.href = url; + a.download = 'erdiagram-layout.json'; + $wnd.document.body.appendChild(a); + a.click(); + $wnd.document.body.removeChild(a); + $wnd.URL.revokeObjectURL(url); + }-*/; + + private static native void clearSavedPositions(String dbuuid, String schemaUUID) /*-{ + var storageKey = 'erdiagram-positions-' + dbuuid + '-' + schemaUUID; + try { + $wnd.localStorage.removeItem(storageKey); + } catch(e) {} + }-*/; } diff --git a/src/main/java/com/databasepreservation/server/Server.gwt.xml b/src/main/java/com/databasepreservation/server/Server.gwt.xml index 2f05d2c76..c4e2d77b3 100644 --- a/src/main/java/com/databasepreservation/server/Server.gwt.xml +++ b/src/main/java/com/databasepreservation/server/Server.gwt.xml @@ -13,5 +13,5 @@ - + diff --git a/src/main/resources/com/databasepreservation/common/public/main.css b/src/main/resources/com/databasepreservation/common/public/main.css index ae5123856..9b42dd40d 100644 --- a/src/main/resources/com/databasepreservation/common/public/main.css +++ b/src/main/resources/com/databasepreservation/common/public/main.css @@ -911,6 +911,32 @@ p[data-value]:after { margin-top: 10%; } +.layoutDropdownPopup { + background-color: #fff; + border: 1px solid #ccc; + border-radius: 4px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + padding: 4px 0; +} + +.layoutDropdownMenu { + min-width: 160px; +} + +.layoutDropdownItem { + padding: 4px 16px; + line-height: 2.5rem; + height: 2.5rem; + cursor: pointer; + font-size: 1.3rem; + white-space: nowrap; + color: #222; +} + +.layoutDropdownItem:hover { + background-color: #f5f5f5; +} + /********************************************** DATABASE SEARCH PAGE ************************************************/ @@ -2844,10 +2870,20 @@ html, body { font-weight: 500; font-size: 1.1em; margin-bottom: 10px; - display: inline-block; + display: block; + width: 100%; + overflow: hidden; line-height: 1em; } +.card-header > div:first-child { + float: left; +} + +.card-header .btn-pull-right { + float: right; +} + .card-diagram { padding: 0; }