diff --git a/modules/AdministrationGUI/src/main/java/org/janelia/workstation/admin/AdministrationTopComponent.java b/modules/AdministrationGUI/src/main/java/org/janelia/workstation/admin/AdministrationTopComponent.java index 74485408a..782aee2a8 100644 --- a/modules/AdministrationGUI/src/main/java/org/janelia/workstation/admin/AdministrationTopComponent.java +++ b/modules/AdministrationGUI/src/main/java/org/janelia/workstation/admin/AdministrationTopComponent.java @@ -77,9 +77,11 @@ private void setupGUI() { setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); this.topMenu = new JPanel(); - BoxLayout layout = new BoxLayout(topMenu, BoxLayout.X_AXIS); - topMenu.setLayout(layout); + topMenu.setLayout(new BoxLayout(topMenu, BoxLayout.Y_AXIS)); // Main panel with vertical layout + // First row with "Users" and "Groups" + JPanel row1 = new JPanel(); + row1.setLayout(new BoxLayout(row1, BoxLayout.X_AXIS)); // top level buttons JButton listUsersButton = new JButton(UIUtils.getClasspathImage(this.getClass(), "/org/janelia/workstation/admin/images/user.png")); listUsersButton.setText("Users"); @@ -87,9 +89,9 @@ private void setupGUI() { listUsersButton.setVerticalTextPosition(SwingConstants.BOTTOM); listUsersButton.setHorizontalTextPosition(SwingConstants.CENTER); listUsersButton.addActionListener(event -> viewUserList()); - topMenu.add(listUsersButton); + row1.add(listUsersButton); - topMenu.add(Box.createHorizontalStrut(20)); + row1.add(Box.createHorizontalStrut(20)); // Add space between buttons JButton listGroupsButton = new JButton(UIUtils.getClasspathImage(this.getClass(), "/org/janelia/workstation/admin/images/group.png")); listGroupsButton.setText("Groups"); @@ -97,17 +99,30 @@ private void setupGUI() { listGroupsButton.setToolTipText("Show all groups"); listGroupsButton.setVerticalTextPosition(SwingConstants.BOTTOM); listGroupsButton.setHorizontalTextPosition(SwingConstants.CENTER); - topMenu.add(listGroupsButton); - - topMenu.add(Box.createHorizontalStrut(20)); + listGroupsButton.addActionListener(event -> viewGroupList()); + row1.add(listGroupsButton); + row1.add(Box.createHorizontalStrut(20)); JButton getLogsButton = new JButton(UIUtils.getClasspathImage(this.getClass(), "/org/janelia/workstation/admin/images/logs.png")); getLogsButton.setText("Logs"); getLogsButton.setToolTipText("Retrieve Logs"); getLogsButton.addActionListener(event -> getLogs()); getLogsButton.setVerticalTextPosition(SwingConstants.BOTTOM); getLogsButton.setHorizontalTextPosition(SwingConstants.CENTER); - topMenu.add(getLogsButton); + row1.add(getLogsButton); + + row1.add(Box.createHorizontalStrut(20)); + + JButton workspaceCleanupButton = new JButton(UIUtils.getClasspathImage(this.getClass(), "/org/janelia/workstation/admin/images/clean.png")); + workspaceCleanupButton.setText("Cleanup Db"); + workspaceCleanupButton.setToolTipText("Manage and delete large workspaces"); + workspaceCleanupButton.setVerticalTextPosition(SwingConstants.BOTTOM); + workspaceCleanupButton.setHorizontalTextPosition(SwingConstants.CENTER); + workspaceCleanupButton.addActionListener(event -> databaseCleanup()); + row1.add(workspaceCleanupButton); + + // Add both rows to the main menu + topMenu.add(row1); add(topMenu); add(Box.createVerticalGlue()); @@ -127,6 +142,15 @@ public void objectsInvalidated(DomainObjectInvalidationEvent event) { } } + void databaseCleanup() { + DatabaseCleanupPanel panel = new DatabaseCleanupPanel(this); + removeAll(); + add(panel); + revalidate(); + repaint(); + this.currentView = panel; + } + void viewUserList() { UserManagementPanel panel = new UserManagementPanel(this); removeAll(); diff --git a/modules/AdministrationGUI/src/main/java/org/janelia/workstation/admin/DatabaseCleanupPanel.java b/modules/AdministrationGUI/src/main/java/org/janelia/workstation/admin/DatabaseCleanupPanel.java new file mode 100644 index 000000000..9e921d507 --- /dev/null +++ b/modules/AdministrationGUI/src/main/java/org/janelia/workstation/admin/DatabaseCleanupPanel.java @@ -0,0 +1,224 @@ +package org.janelia.workstation.admin; + +import org.janelia.model.domain.tiledMicroscope.TmWorkspace; +import org.janelia.model.domain.tiledMicroscope.TmWorkspaceInfo; +import org.janelia.workstation.common.gui.support.DesktopApi; +import org.janelia.workstation.controller.access.TiledMicroscopeDomainMgr; +import org.janelia.workstation.core.util.Refreshable; +import org.janelia.workstation.core.workers.BackgroundWorker; +import org.janelia.workstation.core.workers.SimpleWorker; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.*; +import javax.swing.table.AbstractTableModel; +import java.awt.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; + +public class DatabaseCleanupPanel extends JPanel implements Refreshable { + private static final Logger log = LoggerFactory.getLogger(DatabaseCleanupPanel.class); + private final AdministrationTopComponent parent; + private JTable workspaceTable; + private WorkspaceTableModel tableModel; + + public DatabaseCleanupPanel(AdministrationTopComponent parent) { + this.parent = parent; + refresh(); + } + + private void setupUI() { + setLayout(new BorderLayout()); + removeAll(); + + JPanel titlePanel = new TitlePanel("Manage Workspaces", "Return To Top Menu", + event -> refresh(), + event -> returnHome()); + add(titlePanel, BorderLayout.NORTH); + + tableModel = new WorkspaceTableModel(); + workspaceTable = new JTable(tableModel); + JScrollPane scrollPane = new JScrollPane(workspaceTable); + add(scrollPane, BorderLayout.CENTER); + + JButton refreshButton = new JButton("Load Largest Workspaces"); + refreshButton.addActionListener(e -> loadLargestWorkspaces()); + + JButton deleteButton = new JButton("Delete Selected Workspaces"); + deleteButton.addActionListener(e -> deleteSelectedWorkspaces()); + + JPanel buttonPanel = new JPanel(); + buttonPanel.add(refreshButton); + buttonPanel.add(deleteButton); + add(buttonPanel, BorderLayout.SOUTH); + + revalidate(); + } + + private void loadLargestWorkspaces() { + SimpleWorker worker = new SimpleWorker() { + private List workspaceResults; + + @Override + protected void doStuff() { + TiledMicroscopeDomainMgr domainMgr = TiledMicroscopeDomainMgr.getDomainMgr(); + workspaceResults = domainMgr.getLargestWorkspaces(); + } + + @Override + protected void hadSuccess() { + tableModel.clear(); + tableModel.setWorkspaces(workspaceResults); + } + + @Override + protected void hadError(Throwable error) { + log.error("Error loading workspaces", error); + JOptionPane.showMessageDialog(DatabaseCleanupPanel.this, + "Failed to load workspaces: " + error.getMessage(), "Error", + JOptionPane.ERROR_MESSAGE); + } + }; + worker.execute(); + } + + private void deleteSelectedWorkspaces() { + List selectedWorkspaces = tableModel.getSelectedWorkspaces(); + if (selectedWorkspaces.isEmpty()) { + JOptionPane.showMessageDialog(this, "Please select at least one workspace to delete.", "No Selection", JOptionPane.WARNING_MESSAGE); + return; + } + + int confirmation = JOptionPane.showConfirmDialog(this, + "Are you sure you want to delete the selected workspaces?", + "Confirm Deletion", JOptionPane.YES_NO_OPTION); + + if (confirmation == JOptionPane.YES_OPTION) { + + BackgroundWorker deleter = new BackgroundWorker() { + @Override + public String getName() { + return "Deleting TmWorkspaces"; + } + + @Override + protected void doStuff() { + TiledMicroscopeDomainMgr domainMgr = TiledMicroscopeDomainMgr.getDomainMgr(); + domainMgr.removeWorkspaces(selectedWorkspaces); + } + + @Override + protected void hadSuccess() { + loadLargestWorkspaces(); + JOptionPane.showMessageDialog(DatabaseCleanupPanel.this, + "Selected workspaces deleted successfully.", "Success", JOptionPane.INFORMATION_MESSAGE); + } + + @Override + protected void hadError(Throwable error) { + log.error("Error deleting workspaces", error); + JOptionPane.showMessageDialog(DatabaseCleanupPanel.this, + "Failed to delete workspaces: " + error.getMessage(), "Error", + JOptionPane.ERROR_MESSAGE); + } + }; + deleter.executeWithEvents(); + } + } + + @Override + public void refresh() { + setupUI(); + } + + private void returnHome() { + parent.viewTopMenu(); + } +} + +class WorkspaceTableModel extends AbstractTableModel { + private final String[] columnNames = {"Select", "Name", "Owner", "Date Created", "Size"}; + private List workspaceResults = new ArrayList<>(); + private final List selected = new ArrayList<>(); + + public void setWorkspaces(List workspaceResults) { + this.workspaceResults = workspaceResults; + selected.clear(); + for (int i = 0; i < workspaceResults.size(); i++) { + selected.add(false); // Initialize selection state + } + fireTableDataChanged(); + } + + @Override + public int getRowCount() { + return workspaceResults.size(); + } + + @Override + public int getColumnCount() { + return columnNames.length; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + TmWorkspaceInfo workspaceInfo = workspaceResults.get(rowIndex); + switch (columnIndex) { + case 0: + return selected.get(rowIndex); // Boolean checkbox + case 1: + return workspaceInfo.getWorkspaceName(); + case 2: + return workspaceInfo.getOwnerKey(); + case 3: + return workspaceInfo.getDateCreated(); + case 4: + return workspaceInfo.getTotalSize(); + default: + return null; + } + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + return columnIndex == 0; // Allow only checkboxes to be editable + } + + @Override + public void setValueAt(Object aValue, int rowIndex, int columnIndex) { + if (columnIndex == 0 && aValue instanceof Boolean) { + selected.set(rowIndex, (Boolean) aValue); + fireTableCellUpdated(rowIndex, columnIndex); // Notify JTable of the change + } + } + + public void clear() { + workspaceResults.clear(); + selected.clear(); + fireTableDataChanged(); // Notify the table UI that the data has changed + } + + @Override + public Class getColumnClass(int columnIndex) { + return columnIndex == 0 ? Boolean.class : String.class; + } + + public List getSelectedWorkspaces() { + List selectedWorkspaces = new ArrayList<>(); + for (int i = 0; i < workspaceResults.size(); i++) { + if (selected.get(i)) { + selectedWorkspaces.add(workspaceResults.get(i).getWorkspaceId()); + } + } + return selectedWorkspaces; + } + + @Override + public String getColumnName(int column) { + return columnNames[column]; + } +} + diff --git a/modules/AdministrationGUI/src/main/resources/org/janelia/workstation/admin/images/clean.png b/modules/AdministrationGUI/src/main/resources/org/janelia/workstation/admin/images/clean.png new file mode 100644 index 000000000..ce0815fe1 Binary files /dev/null and b/modules/AdministrationGUI/src/main/resources/org/janelia/workstation/admin/images/clean.png differ diff --git a/modules/AdministrationGUI/src/main/resources/org/janelia/workstation/admin/images/logs.png b/modules/AdministrationGUI/src/main/resources/org/janelia/workstation/admin/images/logs.png index 77b1da29a..ce2743d67 100644 Binary files a/modules/AdministrationGUI/src/main/resources/org/janelia/workstation/admin/images/logs.png and b/modules/AdministrationGUI/src/main/resources/org/janelia/workstation/admin/images/logs.png differ diff --git a/modules/ViewerController/src/main/java/org/janelia/workstation/controller/access/TiledMicroscopeDomainMgr.java b/modules/ViewerController/src/main/java/org/janelia/workstation/controller/access/TiledMicroscopeDomainMgr.java index 2e037dabb..a1cf84b81 100644 --- a/modules/ViewerController/src/main/java/org/janelia/workstation/controller/access/TiledMicroscopeDomainMgr.java +++ b/modules/ViewerController/src/main/java/org/janelia/workstation/controller/access/TiledMicroscopeDomainMgr.java @@ -15,6 +15,7 @@ import org.janelia.workstation.core.events.Events; import org.janelia.workstation.core.events.lifecycle.ConsolePropsLoaded; import org.janelia.model.domain.tiledMicroscope.BoundingBox3d; +import org.janelia.model.domain.tiledMicroscope.TmWorkspaceInfo; import org.janelia.workstation.integration.util.FrameworkAccess; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -153,6 +154,11 @@ public Collection getWorkspaceBoundingBoxes(Long workspaceId) thr return client.getWorkspaceBoundingBoxes(workspaceId); } + public List getLargestWorkspaces () { + LOG.debug("getLargestWorkspaces()"); + return client.getLargestWorkspaces(AccessManager.getSubjectKey()); + } + public List getOperationLogs (Long workspaceId, Long neuronId, Date startTime, Date endTime, String subjectKey) { LOG.debug("getOperationLogs(workspaceId={}, neuronId={}, startTime={}, endTime={})", @@ -231,6 +237,11 @@ public void remove(TmWorkspace workspace) throws Exception { getModel().notifyDomainObjectRemoved(workspace); } + public void removeWorkspaces(List selectedWorkspaces) { + LOG.debug("removeWorkspaces({})", selectedWorkspaces); + client.removeWorkspaces(selectedWorkspaces); + } + class RetrieveNeuronsTask extends RecursiveTask> { double defaultLength = 50000.0; int start; diff --git a/modules/ViewerController/src/main/java/org/janelia/workstation/controller/access/TiledMicroscopeRestClient.java b/modules/ViewerController/src/main/java/org/janelia/workstation/controller/access/TiledMicroscopeRestClient.java index 217903c64..eb338b110 100644 --- a/modules/ViewerController/src/main/java/org/janelia/workstation/controller/access/TiledMicroscopeRestClient.java +++ b/modules/ViewerController/src/main/java/org/janelia/workstation/controller/access/TiledMicroscopeRestClient.java @@ -15,6 +15,7 @@ import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.io.ByteStreams; @@ -118,6 +119,27 @@ public TmSample create(TmSample tmSample, Map storageAttributes) return response.readEntity(TmSample.class); } + public List getLargestWorkspaces(String subjectKey ) { + WebTarget target = getMouselightDataEndpoint("/workspace/largest") + .queryParam("username", subjectKey); + Response response = target + .request("application/json") + .get(); + checkBadResponse(target, response); + try { + String jsonResponse = response.readEntity(String.class); + + + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.readValue(jsonResponse, new TypeReference>() { + }); + } catch (Exception e) { + FrameworkAccess.handleException(e); + LOG.error ("Problems returning the largest workspaces in the db"); + throw new RemoteServiceException("Client had problems retrieving largest workspaces"); + } + } + public List getOperationLogs(Long workspaceId, Long neuronId, Date startTime, Date endTime, String subjectKey ) { @@ -501,4 +523,13 @@ public boolean isServerPathAvailable(String serverPath, boolean directoryOnly, M int responseStatus = response.getStatus(); return responseStatus == 200; } + + public void removeWorkspaces(List selectedWorkspaces) { + WebTarget target = getMouselightDataEndpoint("/workspaces/remove"); + + Response response = target + .request("application/json") + .put(Entity.json(selectedWorkspaces)); + checkBadResponse(target, response); + } } diff --git a/pom.xml b/pom.xml index 1b55413d4..f2a949e61 100644 --- a/pom.xml +++ b/pom.xml @@ -109,7 +109,7 @@ 9.4.46.v20220331 - 3.3.0 + 3.3.4 2.5.0