1 /* 2 * ConnectionsPane.java 3 * 4 * Copyright (C) 2021 by RStudio, PBC 5 * 6 * Unless you have received this program directly from RStudio pursuant 7 * to the terms of a commercial license agreement with RStudio, then 8 * this program is licensed to you under the terms of version 3 of the 9 * GNU Affero General Public License. This program is distributed WITHOUT 10 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT, 11 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the 12 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details. 13 * 14 */ 15 package org.rstudio.studio.client.workbench.views.connections.ui; 16 17 import java.util.ArrayList; 18 import java.util.Collections; 19 import java.util.Comparator; 20 import java.util.List; 21 22 import com.google.gwt.core.client.GWT; 23 import com.google.gwt.core.client.JsArray; 24 import com.google.gwt.core.client.Scheduler; 25 import com.google.gwt.dom.client.Style.Unit; 26 import com.google.gwt.event.dom.client.ClickEvent; 27 import com.google.gwt.event.dom.client.ClickHandler; 28 import com.google.gwt.event.dom.client.HasClickHandlers; 29 import com.google.gwt.event.logical.shared.ValueChangeHandler; 30 import com.google.gwt.event.shared.HandlerRegistration; 31 import com.google.gwt.resources.client.ImageResource; 32 import com.google.gwt.user.cellview.client.Column; 33 import com.google.gwt.user.cellview.client.DataGrid; 34 import com.google.gwt.user.cellview.client.TextColumn; 35 import com.google.gwt.user.cellview.client.TextHeader; 36 import com.google.gwt.user.client.ui.Image; 37 import com.google.gwt.user.client.ui.MenuItem; 38 import com.google.gwt.user.client.ui.SuggestOracle; 39 import com.google.gwt.user.client.ui.Widget; 40 import com.google.gwt.view.client.ListDataProvider; 41 import com.google.gwt.view.client.ProvidesKey; 42 import com.google.gwt.view.client.SelectionChangeEvent; 43 import com.google.gwt.view.client.SingleSelectionModel; 44 import com.google.inject.Inject; 45 46 import org.rstudio.core.client.BrowseCap; 47 import org.rstudio.core.client.StringUtil; 48 import org.rstudio.core.client.cellview.ImageButtonColumn; 49 import org.rstudio.core.client.command.AppCommand; 50 import org.rstudio.core.client.command.VisibleChangedEvent; 51 import org.rstudio.core.client.command.VisibleChangedHandler; 52 import org.rstudio.core.client.resources.ImageResource2x; 53 import org.rstudio.core.client.theme.RStudioDataGridResources; 54 import org.rstudio.core.client.theme.RStudioDataGridStyle; 55 import org.rstudio.core.client.theme.res.ThemeStyles; 56 import org.rstudio.core.client.widget.Base64ImageCell; 57 import org.rstudio.core.client.widget.DecorativeImage; 58 import org.rstudio.core.client.widget.OperationWithInput; 59 import org.rstudio.core.client.widget.RStudioDataGrid; 60 import org.rstudio.core.client.widget.SearchWidget; 61 import org.rstudio.core.client.widget.SecondaryToolbar; 62 import org.rstudio.core.client.widget.SlidingLayoutPanel; 63 import org.rstudio.core.client.widget.Toolbar; 64 import org.rstudio.core.client.widget.ToolbarButton; 65 import org.rstudio.core.client.widget.ToolbarLabel; 66 import org.rstudio.core.client.widget.ToolbarMenuButton; 67 import org.rstudio.core.client.widget.ToolbarPopupMenu; 68 import org.rstudio.studio.client.application.events.EventBus; 69 import org.rstudio.studio.client.workbench.commands.Commands; 70 import org.rstudio.studio.client.workbench.prefs.model.UserPrefs; 71 import org.rstudio.studio.client.workbench.ui.WorkbenchPane; 72 import org.rstudio.studio.client.workbench.views.connections.ConnectionsPresenter; 73 import org.rstudio.studio.client.workbench.views.connections.events.ActiveConnectionsChangedEvent; 74 import org.rstudio.studio.client.workbench.views.connections.events.ExecuteConnectionActionEvent; 75 import org.rstudio.studio.client.workbench.views.connections.events.ExecuteConnectionActionEvent.Handler; 76 import org.rstudio.studio.client.workbench.views.connections.events.ExploreConnectionEvent; 77 import org.rstudio.studio.client.workbench.views.connections.events.PerformConnectionEvent; 78 import org.rstudio.studio.client.workbench.views.connections.model.Connection; 79 import org.rstudio.studio.client.workbench.views.connections.model.ConnectionAction; 80 import org.rstudio.studio.client.workbench.views.connections.model.ConnectionId; 81 import org.rstudio.studio.client.workbench.views.connections.model.ConnectionOptions; 82 83 public class ConnectionsPane extends WorkbenchPane 84 implements ConnectionsPresenter.Display, 85 ActiveConnectionsChangedEvent.Handler 86 { 87 @Inject ConnectionsPane(Commands commands, EventBus eventBus, UserPrefs userPrefs)88 public ConnectionsPane(Commands commands, EventBus eventBus, UserPrefs userPrefs) 89 { 90 // initialize 91 super("Connections", eventBus); 92 commands_ = commands; 93 userPrefs_ = userPrefs; 94 95 // track activation events to update the toolbar 96 events_.addHandler(ActiveConnectionsChangedEvent.TYPE, this); 97 98 // create data grid 99 keyProvider_ = new ProvidesKey<Connection>() { 100 @Override 101 public Object getKey(Connection connection) 102 { 103 return connection.hashCode(); 104 } 105 }; 106 107 selectionModel_ = new SingleSelectionModel<>(); 108 connectionsDataGrid_ = new RStudioDataGrid<>(1000, RES, keyProvider_); 109 connectionsDataGrid_.setSelectionModel(selectionModel_); 110 selectionModel_.addSelectionChangeHandler(new SelectionChangeEvent.Handler() 111 { 112 @Override 113 public void onSelectionChange(SelectionChangeEvent event) 114 { 115 Connection selectedConnection = selectionModel_.getSelectedObject(); 116 if (selectedConnection != null) 117 fireEvent(new ExploreConnectionEvent(selectedConnection)); 118 } 119 }); 120 121 // add type column; this is a package-provided image we scale to 16x16 122 typeColumn_ = new Column<Connection, String>(new Base64ImageCell(16, 16)) 123 { 124 @Override 125 public String getValue(Connection connection) 126 { 127 if (StringUtil.isNullOrEmpty(connection.getIconData())) 128 return null; 129 return connection.getIconData(); 130 } 131 }; 132 133 connectionsDataGrid_.addColumn(typeColumn_, new TextHeader("")); 134 connectionsDataGrid_.setColumnWidth(typeColumn_, 20, Unit.PX); 135 136 // add host column 137 hostColumn_ = new TextColumn<Connection>() { 138 @Override 139 public String getValue(Connection connection) 140 { 141 return connection.getDisplayName(); 142 } 143 }; 144 connectionsDataGrid_.addColumn(hostColumn_, new TextHeader("Connection")); 145 connectionsDataGrid_.setColumnWidth(hostColumn_, 50, Unit.PCT); 146 147 // add status column 148 statusColumn_ = new TextColumn<Connection>() { 149 150 @Override 151 public String getValue(Connection connection) 152 { 153 if (isConnected(connection.getId())) 154 return "Connected"; 155 else 156 return ""; 157 } 158 }; 159 statusColumn_.setCellStyleNames(RES.dataGridStyle().statusColumn()); 160 connectionsDataGrid_.addColumn(statusColumn_, new TextHeader("Status")); 161 connectionsDataGrid_.setColumnWidth(statusColumn_, 75, Unit.PX); 162 163 // add explore column 164 ImageButtonColumn<Connection> exploreColumn = 165 new ImageButtonColumn<Connection>( 166 new ImageResource2x(RES.connectionExploreButton2x()), 167 new OperationWithInput<Connection>() { 168 @Override 169 public void execute(Connection connection) 170 { 171 fireEvent(new ExploreConnectionEvent(connection)); 172 } 173 }, 174 "Explore connection") { 175 }; 176 connectionsDataGrid_.addColumn(exploreColumn, new TextHeader("")); 177 connectionsDataGrid_.setColumnWidth(exploreColumn, 30, Unit.PX); 178 179 // data provider 180 dataProvider_ = new ListDataProvider<>(); 181 dataProvider_.addDataDisplay(connectionsDataGrid_); 182 183 // create connection explorer, add it, and hide it 184 connectionExplorer_ = new ConnectionExplorer(); 185 connectionExplorer_.setSize("100%", "100%"); 186 187 // create main panel 188 mainPanel_ = new SlidingLayoutPanel(connectionsDataGrid_, connectionExplorer_); 189 mainPanel_.addStyleName("ace_editor_theme"); 190 191 // create widget 192 ensureWidget(); 193 194 setSecondaryToolbarVisible(false); 195 } 196 197 @Override setConnections(List<Connection> connections)198 public void setConnections(List<Connection> connections) 199 { 200 dataProvider_.setList(connections); 201 sortConnections(); 202 } 203 204 @Override setActiveConnections(List<ConnectionId> connections)205 public void setActiveConnections(List<ConnectionId> connections) 206 { 207 // update active connection 208 activeConnections_ = connections; 209 sortConnections(); 210 211 // redraw the data grid 212 connectionsDataGrid_.redraw(); 213 214 // update explored connection 215 connectionExplorer_.setConnected(exploredConnection_ != null && 216 isConnected(exploredConnection_.getId())); 217 } 218 isConnected(ConnectionId id)219 private boolean isConnected(ConnectionId id) 220 { 221 for (int i=0; i<activeConnections_.size(); i++) 222 if (activeConnections_.get(i).equalTo(id)) 223 return true; 224 return false; 225 } 226 227 @Override getSearchFilter()228 public String getSearchFilter() 229 { 230 return searchWidget_.getValue(); 231 } 232 233 @Override addSearchFilterChangeHandler( ValueChangeHandler<String> handler)234 public HandlerRegistration addSearchFilterChangeHandler( 235 ValueChangeHandler<String> handler) 236 { 237 return searchWidget_.addValueChangeHandler(handler); 238 } 239 240 @Override addExploreConnectionHandler( ExploreConnectionEvent.Handler handler)241 public HandlerRegistration addExploreConnectionHandler( 242 ExploreConnectionEvent.Handler handler) 243 { 244 return addHandler(handler, ExploreConnectionEvent.TYPE); 245 } 246 247 @Override addExecuteConnectionActionHandler(Handler handler)248 public HandlerRegistration addExecuteConnectionActionHandler(Handler handler) 249 { 250 return addHandler(handler, ExecuteConnectionActionEvent.TYPE); 251 } 252 253 @Override showConnectionExplorer(final Connection connection, String connectVia)254 public void showConnectionExplorer(final Connection connection, 255 String connectVia) 256 { 257 selectionModel_.clear(); 258 259 setConnection(connection, connectVia); 260 261 installConnectionExplorerToolbar(connection); 262 263 // show the right panel (connection explorer) 264 mainPanel_.slideWidgets( 265 SlidingLayoutPanel.Direction.SlideRight, !userPrefs_.reducedMotion().getValue(), () -> 266 { 267 connectionExplorer_.onResize(); 268 }); 269 } 270 271 @Override setExploredConnection(Connection connection)272 public void setExploredConnection(Connection connection) 273 { 274 setConnection(connection, connectionExplorer_.getConnectVia()); 275 } 276 setConnection(Connection connection, String connectVia)277 private void setConnection(Connection connection, String connectVia) 278 { 279 exploredConnection_ = connection; 280 281 if (exploredConnection_ != null) 282 { 283 connectionExplorer_.setConnection(connection, connectVia); 284 connectionExplorer_.setConnected(isConnected(connection.getId())); 285 } 286 } 287 288 @Override updateExploredConnection(String hint)289 public void updateExploredConnection(String hint) 290 { 291 connectionExplorer_.updateObjectBrowser(hint); 292 } 293 294 @Override showConnectionsList(boolean animate)295 public void showConnectionsList(boolean animate) 296 { 297 exploredConnection_ = null; 298 299 installConnectionsToolbar(); 300 301 // show the left panel (connection explorer) 302 mainPanel_.slideWidgets( 303 SlidingLayoutPanel.Direction.SlideLeft, animate, () -> {}); 304 } 305 306 307 @Override backToConnectionsButton()308 public HasClickHandlers backToConnectionsButton() 309 { 310 return backToConnectionsButton_; 311 } 312 313 @Override getConnectVia()314 public String getConnectVia() 315 { 316 return connectionExplorer_.getConnectVia(); 317 } 318 319 @Override getConnectCode()320 public String getConnectCode() 321 { 322 return connectionExplorer_.getConnectCode(); 323 } 324 325 @Override showConnectionProgress(String message)326 public void showConnectionProgress(String message) 327 { 328 connectionExplorer_.showConnectionProgress(message); 329 } 330 331 @Override onResize()332 public void onResize() 333 { 334 connectionExplorer_.onResize(); 335 } 336 337 @Override createMainToolbar()338 protected Toolbar createMainToolbar() 339 { 340 toolbar_ = new Toolbar("Connections Tab"); 341 342 searchWidget_ = new SearchWidget("Filter by connection", new SuggestOracle() { 343 @Override 344 public void requestSuggestions(Request request, Callback callback) 345 { 346 // no suggestions 347 callback.onSuggestionsReady( 348 request, 349 new Response(new ArrayList<>())); 350 } 351 }); 352 353 objectSearchWidget_ = new SearchWidget("Filter by object", new SuggestOracle() { 354 @Override 355 public void requestSuggestions(Request request, Callback callback) 356 { 357 // no suggestions 358 callback.onSuggestionsReady( 359 request, 360 new Response(new ArrayList<>())); 361 } 362 }); 363 364 objectSearchWidget_.addValueChangeHandler(event -> 365 connectionExplorer_.setFilterText(event.getValue())); 366 367 backToConnectionsButton_ = new ToolbarButton( 368 ToolbarButton.NoText, 369 "View all connections", 370 commands_.helpBack().getImageResource()); 371 372 // connect meuu 373 ToolbarPopupMenu connectMenu = new ToolbarPopupMenu(); 374 connectMenu.addItem(connectMenuItem( 375 commands_.historySendToConsole().getImageResource(), 376 "R Console", 377 ConnectionOptions.CONNECT_R_CONSOLE)); 378 connectMenu.addSeparator(); 379 connectMenu.addItem(connectMenuItem( 380 commands_.newSourceDoc().getImageResource(), 381 "New R Script", 382 ConnectionOptions.CONNECT_NEW_R_SCRIPT)); 383 connectMenu.addItem(connectMenuItem( 384 commands_.newRNotebook().getImageResource(), 385 "New R Notebook", 386 ConnectionOptions.CONNECT_NEW_R_NOTEBOOK)); 387 if (BrowseCap.INSTANCE.canCopyToClipboard()) 388 { 389 connectMenu.addSeparator(); 390 connectMenu.addItem(connectMenuItem( 391 commands_.copyPlotToClipboard().getImageResource(), 392 "Copy to Clipboard", 393 ConnectionOptions.CONNECT_COPY_TO_CLIPBOARD)); 394 } 395 connectMenuButton_ = new ToolbarMenuButton( 396 "Connect", 397 ToolbarButton.NoTitle, 398 commands_.newConnection().getImageResource(), 399 connectMenu); 400 401 // manage connect menu visibility 402 connectMenuButton_.setVisible(!commands_.disconnectConnection().isVisible()); 403 commands_.disconnectConnection().addVisibleChangedHandler( 404 new VisibleChangedHandler() { 405 @Override 406 public void onVisibleChanged(VisibleChangedEvent event) 407 { 408 connectMenuButton_.setVisible( 409 !commands_.disconnectConnection().isVisible()); 410 } 411 }); 412 413 installConnectionsToolbar(); 414 415 return toolbar_; 416 } 417 418 @Override createSecondaryToolbar()419 protected SecondaryToolbar createSecondaryToolbar() 420 { 421 secondaryToolbar_ = new SecondaryToolbar("Connections Tab Connection"); 422 secondaryToolbar_.addLeftWidget(connectionName_ = new ToolbarLabel()); 423 connectionIcon_ = new Image(); 424 connectionIcon_.setWidth("16px"); 425 connectionIcon_.setHeight("16px"); 426 connectionIcon_.setAltText(""); // decorative image 427 connectionType_ = new ToolbarLabel(); 428 connectionType_.getElement().getStyle().setMarginLeft(5, Unit.PX); 429 connectionType_.getElement().getStyle().setMarginRight(10, Unit.PX); 430 secondaryToolbar_.addRightWidget(connectionIcon_); 431 secondaryToolbar_.addRightWidget(connectionType_); 432 433 ThemeStyles styles = ThemeStyles.INSTANCE; 434 secondaryToolbar_.getWrapper().addStyleName(styles.tallerToolbarWrapper()); 435 436 return secondaryToolbar_; 437 } 438 439 @Override createMainWidget()440 protected Widget createMainWidget() 441 { 442 return mainPanel_; 443 } 444 445 @Override onBeforeSelected()446 public void onBeforeSelected() 447 { 448 super.onBeforeSelected(); 449 connectionsDataGrid_.redraw(); 450 } 451 connectMenuItem(ImageResource icon, String text, final String connectVia)452 private MenuItem connectMenuItem(ImageResource icon, 453 String text, 454 final String connectVia) 455 { 456 return new MenuItem( 457 AppCommand.formatMenuLabel(icon, text, null), 458 true, 459 new Scheduler.ScheduledCommand() { 460 461 @Override 462 public void execute() 463 { 464 events_.fireEvent( 465 new PerformConnectionEvent( 466 connectVia, 467 connectionExplorer_.getConnectCode())); 468 } 469 }); 470 } 471 472 473 private void installConnectionsToolbar() 474 { 475 toolbar_.removeAllWidgets(); 476 477 toolbar_.addLeftWidget(commands_.newConnection().createToolbarButton()); 478 479 toolbar_.addLeftSeparator(); 480 481 toolbar_.addRightWidget(searchWidget_); 482 483 setSecondaryToolbarVisible(false); 484 } 485 486 private void installConnectionExplorerToolbar(final Connection connection) 487 { 488 toolbar_.removeAllWidgets(); 489 490 toolbar_.addLeftWidget(backToConnectionsButton_); 491 toolbar_.addLeftSeparator(); 492 493 toolbar_.addLeftWidget(connectMenuButton_); 494 495 toolbar_.addLeftSeparator(); 496 497 if (isConnected(connection.getId()) && connection.getActions() != null) 498 { 499 // if we have any actions, create a toolbar button for each one 500 for (int i = 0; i < connection.getActions().length(); i++) 501 { 502 final ConnectionAction action = connection.getActions().get(i); 503 504 // use the supplied base64 icon data if it was provided 505 DecorativeImage icon = StringUtil.isNullOrEmpty(action.getIconData()) ? 506 null : 507 new DecorativeImage(action.getIconData()); 508 509 // force to 20x18 510 if (icon != null) 511 { 512 icon.setWidth("20px"); 513 icon.setHeight("18px"); 514 } 515 516 ToolbarButton button = new ToolbarButton(action.getName(), 517 ToolbarButton.NoTitle, 518 icon, // left image 519 null, // right image 520 // invoke the action when the button is clicked 521 new ClickHandler() 522 { 523 @Override 524 public void onClick(ClickEvent arg0) 525 { 526 fireEvent(new ExecuteConnectionActionEvent( 527 connection.getId(), action.getName())); 528 } 529 }); 530 531 // move the toolbar button up 5px to account for missing icon if 532 // none was supplied 533 if (StringUtil.isNullOrEmpty(action.getIconData())) 534 button.getElement().getStyle().setMarginTop(-5, Unit.PX); 535 toolbar_.addLeftWidget(button); 536 toolbar_.addLeftSeparator(); 537 } 538 } 539 540 toolbar_.addLeftWidget(commands_.disconnectConnection().createToolbarButton()); 541 542 toolbar_.addRightWidget(commands_.removeConnection().createToolbarButton()); 543 544 ToolbarButton refreshButton = commands_.refreshConnection().createToolbarButton(); 545 refreshButton.addStyleName(ThemeStyles.INSTANCE.refreshToolbarButton()); 546 toolbar_.addRightWidget(refreshButton); 547 548 connectionName_.setText(connection.getDisplayName()); 549 connectionIcon_.setUrl(connection.getIconData()); 550 connectionType_.setText(connection.getId().getType()); 551 552 toolbar_.addRightWidget(objectSearchWidget_); 553 554 setSecondaryToolbarVisible(true); 555 } 556 557 private void sortConnections() 558 { 559 // order the list 560 List<Connection> connections = dataProvider_.getList(); 561 Collections.sort(connections, new Comparator<Connection>() { 562 @Override 563 public int compare(Connection conn1, Connection conn2) 564 { 565 // values to use in comparison 566 boolean conn1Connected = isConnected(conn1.getId()); 567 boolean conn2Connected = isConnected(conn2.getId()); 568 569 if (conn1Connected && !conn2Connected) 570 return -1; 571 else if (conn2Connected && !conn1Connected) 572 return 1; 573 else 574 return -1 * Double.compare(conn1.getLastUsed(), conn2.getLastUsed()); 575 } 576 }); 577 } 578 579 580 private Toolbar toolbar_; 581 private final SlidingLayoutPanel mainPanel_; 582 private final DataGrid<Connection> connectionsDataGrid_; 583 private final SingleSelectionModel<Connection> selectionModel_; 584 private final ConnectionExplorer connectionExplorer_; 585 586 private Connection exploredConnection_ = null; 587 588 private final Column<Connection, String> typeColumn_; 589 private final TextColumn<Connection> hostColumn_; 590 private final TextColumn<Connection> statusColumn_; 591 592 private final ProvidesKey<Connection> keyProvider_; 593 private final ListDataProvider<Connection> dataProvider_; 594 private List<ConnectionId> activeConnections_ = new ArrayList<>(); 595 596 private SearchWidget searchWidget_; 597 private SearchWidget objectSearchWidget_; 598 private ToolbarButton backToConnectionsButton_; 599 private ToolbarMenuButton connectMenuButton_; 600 601 private SecondaryToolbar secondaryToolbar_; 602 private ToolbarLabel connectionName_; 603 private Image connectionIcon_; 604 private ToolbarLabel connectionType_; 605 606 private final Commands commands_; 607 private final UserPrefs userPrefs_; 608 609 // Resources, etc ---- 610 public interface Resources extends RStudioDataGridResources 611 { 612 @Source({RStudioDataGridStyle.RSTUDIO_DEFAULT_CSS, "ConnectionsListDataGridStyle.css"}) 613 Styles dataGridStyle(); 614 615 @Source("connectionExploreButton_2x.png") 616 ImageResource connectionExploreButton2x(); 617 } 618 619 public interface Styles extends RStudioDataGridStyle 620 { 621 String statusColumn(); 622 } 623 624 private static final Resources RES = GWT.create(Resources.class); 625 626 static { 627 RES.dataGridStyle().ensureInjected(); 628 } 629 630 @Override 631 public void onActiveConnectionsChanged(ActiveConnectionsChangedEvent event) 632 { 633 activeConnections_.clear(); 634 635 JsArray<ConnectionId> connections = event.getActiveConnections(); 636 for (int idxConn = 0; idxConn < connections.length(); idxConn++) { 637 activeConnections_.add(connections.get(idxConn)); 638 } 639 640 sortConnections(); 641 642 if (exploredConnection_ != null) 643 installConnectionExplorerToolbar(exploredConnection_); 644 else 645 installConnectionsToolbar(); 646 } 647 } 648