1 /* 2 * ConnectionsPresenter.java 3 * 4 * Copyright (C) 2021 by RStudio, PBC 5 * 6 * This program is licensed to you under the terms of version 3 of the 7 * GNU Affero General Public License. This program is distributed WITHOUT 8 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT, 9 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the 10 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details. 11 * 12 */ 13 package org.rstudio.studio.client.workbench.views.connections; 14 15 import java.util.ArrayList; 16 import java.util.List; 17 18 import com.google.gwt.core.client.JsArray; 19 import com.google.gwt.event.dom.client.ClickEvent; 20 import com.google.gwt.event.dom.client.ClickHandler; 21 import com.google.gwt.event.dom.client.HasClickHandlers; 22 import com.google.gwt.event.logical.shared.ValueChangeEvent; 23 import com.google.gwt.event.logical.shared.ValueChangeHandler; 24 import com.google.gwt.event.shared.HandlerRegistration; 25 import com.google.inject.Inject; 26 27 import org.rstudio.core.client.Debug; 28 import org.rstudio.core.client.ListUtil; 29 import org.rstudio.core.client.ListUtil.FilterPredicate; 30 import org.rstudio.core.client.command.CommandBinder; 31 import org.rstudio.core.client.command.Handler; 32 import org.rstudio.core.client.dom.DomUtils; 33 import org.rstudio.core.client.js.JsObject; 34 import org.rstudio.core.client.widget.MessageDialog; 35 import org.rstudio.core.client.widget.Operation; 36 import org.rstudio.core.client.widget.ProgressIndicator; 37 import org.rstudio.core.client.widget.ProgressOperationWithInput; 38 import org.rstudio.studio.client.application.ApplicationInterrupt; 39 import org.rstudio.studio.client.application.events.EventBus; 40 import org.rstudio.studio.client.common.DelayedProgressRequestCallback; 41 import org.rstudio.studio.client.common.GlobalDisplay; 42 import org.rstudio.studio.client.common.GlobalProgressDelayer; 43 import org.rstudio.studio.client.server.ServerError; 44 import org.rstudio.studio.client.server.VoidServerRequestCallback; 45 import org.rstudio.studio.client.workbench.WorkbenchListManager; 46 import org.rstudio.studio.client.workbench.WorkbenchView; 47 import org.rstudio.studio.client.workbench.commands.Commands; 48 import org.rstudio.studio.client.workbench.model.ClientState; 49 import org.rstudio.studio.client.workbench.model.Session; 50 import org.rstudio.studio.client.workbench.model.SessionInfo; 51 import org.rstudio.studio.client.workbench.model.helper.JSObjectStateValue; 52 import org.rstudio.studio.client.workbench.prefs.model.UserPrefs; 53 import org.rstudio.studio.client.workbench.prefs.model.UserState; 54 import org.rstudio.studio.client.workbench.views.BasePresenter; 55 import org.rstudio.studio.client.workbench.views.connections.events.ActiveConnectionsChangedEvent; 56 import org.rstudio.studio.client.workbench.views.connections.events.ConnectionListChangedEvent; 57 import org.rstudio.studio.client.workbench.views.connections.events.ConnectionOpenedEvent; 58 import org.rstudio.studio.client.workbench.views.connections.events.ConnectionUpdatedEvent; 59 import org.rstudio.studio.client.workbench.views.connections.events.ExecuteConnectionActionEvent; 60 import org.rstudio.studio.client.workbench.views.connections.events.ExploreConnectionEvent; 61 import org.rstudio.studio.client.workbench.views.connections.events.PerformConnectionEvent; 62 import org.rstudio.studio.client.workbench.views.connections.events.ViewConnectionDatasetEvent; 63 import org.rstudio.studio.client.workbench.views.connections.model.Connection; 64 import org.rstudio.studio.client.workbench.views.connections.model.ConnectionId; 65 import org.rstudio.studio.client.workbench.views.connections.model.ConnectionOptions; 66 import org.rstudio.studio.client.workbench.views.connections.model.ConnectionUpdateResult; 67 import org.rstudio.studio.client.workbench.views.connections.model.ConnectionsServerOperations; 68 import org.rstudio.studio.client.workbench.views.connections.model.NewConnectionContext; 69 import org.rstudio.studio.client.workbench.views.connections.ui.NewConnectionWizard; 70 import org.rstudio.studio.client.workbench.views.console.events.SendToConsoleEvent; 71 import org.rstudio.studio.client.workbench.views.source.events.NewDocumentWithCodeEvent; 72 import org.rstudio.studio.client.workbench.views.source.model.SourcePosition; 73 74 public class ConnectionsPresenter extends BasePresenter 75 implements PerformConnectionEvent.Handler, 76 ViewConnectionDatasetEvent.Handler 77 { 78 public interface Display extends WorkbenchView 79 { showConnectionsList(boolean animate)80 void showConnectionsList(boolean animate); 81 setConnections(List<Connection> connections)82 void setConnections(List<Connection> connections); setActiveConnections(List<ConnectionId> connections)83 void setActiveConnections(List<ConnectionId> connections); 84 getSearchFilter()85 String getSearchFilter(); 86 addSearchFilterChangeHandler( ValueChangeHandler<String> handler)87 HandlerRegistration addSearchFilterChangeHandler( 88 ValueChangeHandler<String> handler); 89 addExploreConnectionHandler( ExploreConnectionEvent.Handler handler)90 HandlerRegistration addExploreConnectionHandler( 91 ExploreConnectionEvent.Handler handler); 92 addExecuteConnectionActionHandler( ExecuteConnectionActionEvent.Handler handler)93 HandlerRegistration addExecuteConnectionActionHandler( 94 ExecuteConnectionActionEvent.Handler handler); 95 showConnectionExplorer(Connection connection, String connectVia)96 void showConnectionExplorer(Connection connection, String connectVia); setExploredConnection(Connection connection)97 void setExploredConnection(Connection connection); 98 updateExploredConnection(String hint)99 void updateExploredConnection(String hint); 100 backToConnectionsButton()101 HasClickHandlers backToConnectionsButton(); 102 getConnectVia()103 String getConnectVia(); getConnectCode()104 String getConnectCode(); 105 showConnectionProgress(String message)106 void showConnectionProgress(String message); 107 } 108 109 public interface Binder extends CommandBinder<Commands, ConnectionsPresenter> {} 110 111 @Inject ConnectionsPresenter(Display display, ConnectionsServerOperations server, GlobalDisplay globalDisplay, EventBus eventBus, UserPrefs userPrefs, UserState userState, Binder binder, final Commands commands, WorkbenchListManager listManager, Session session, ApplicationInterrupt applicationInterrupt)112 public ConnectionsPresenter(Display display, 113 ConnectionsServerOperations server, 114 GlobalDisplay globalDisplay, 115 EventBus eventBus, 116 UserPrefs userPrefs, 117 UserState userState, 118 Binder binder, 119 final Commands commands, 120 WorkbenchListManager listManager, 121 Session session, 122 ApplicationInterrupt applicationInterrupt) 123 { 124 super(display); 125 binder.bind(commands, this); 126 display_ = display; 127 commands_ = commands; 128 server_ = server; 129 state_ = userState; 130 userPrefs_ = userPrefs; 131 globalDisplay_ = globalDisplay; 132 eventBus_ = eventBus; 133 applicationInterrupt_ = applicationInterrupt; 134 135 // search filter 136 display_.addSearchFilterChangeHandler(new ValueChangeHandler<String>() { 137 138 @Override 139 public void onValueChange(ValueChangeEvent<String> event) 140 { 141 display_.setConnections(filteredConnections()); 142 } 143 }); 144 145 display_.addExploreConnectionHandler(new ExploreConnectionEvent.Handler() 146 { 147 @Override 148 public void onExploreConnection(ExploreConnectionEvent event) 149 { 150 exploreConnection(event.getConnection()); 151 } 152 }); 153 154 display_.backToConnectionsButton().addClickHandler(new ClickHandler() { 155 156 @Override 157 public void onClick(ClickEvent event) 158 { 159 showAllConnections(!userPrefs_.reducedMotion().getValue()); 160 } 161 }); 162 163 164 display_.addExecuteConnectionActionHandler( 165 new ExecuteConnectionActionEvent.Handler() 166 { 167 168 @Override 169 public void onExecuteConnectionAction( 170 ExecuteConnectionActionEvent event) 171 { 172 server_.connectionExecuteAction(event.getConnectionId(), 173 event.getAction(), new VoidServerRequestCallback()); 174 } 175 }); 176 177 // events 178 eventBus_.addHandler(PerformConnectionEvent.TYPE, this); 179 eventBus_.addHandler(ViewConnectionDatasetEvent.TYPE, this); 180 181 // set connections 182 final SessionInfo sessionInfo = session.getSessionInfo(); 183 updateConnections(sessionInfo.getConnectionList()); 184 updateActiveConnections(sessionInfo.getActiveConnections()); 185 186 // make the explored connection persistent 187 new JSObjectStateValue(MODULE_CONNECTIONS, 188 KEY_EXPLORED_CONNECTION, 189 ClientState.PERSISTENT, 190 session.getSessionInfo().getClientState(), 191 false) 192 { 193 @Override 194 protected void onInit(JsObject value) 195 { 196 // get the value 197 if (value != null) 198 exploredConnection_ = value.cast(); 199 else 200 exploredConnection_ = null; 201 202 lastExploredConnection_ = exploredConnection_; 203 204 // if there is an an explored connection then explore it 205 // (but delay to allow for the panel to be laid out) 206 if (exploredConnection_ != null) 207 exploreConnection(exploredConnection_); 208 } 209 210 @Override 211 protected JsObject getValue() 212 { 213 if (exploredConnection_ != null) 214 return exploredConnection_.cast(); 215 else 216 return null; 217 } 218 219 @Override 220 protected boolean hasChanged() 221 { 222 if (lastExploredConnection_ != exploredConnection_) 223 { 224 lastExploredConnection_ = exploredConnection_; 225 return true; 226 } 227 else 228 { 229 return false; 230 } 231 } 232 }; 233 } 234 activate()235 public void activate() 236 { 237 display_.bringToFront(); 238 } 239 onConnectionOpened(ConnectionOpenedEvent event)240 public void onConnectionOpened(ConnectionOpenedEvent event) 241 { 242 if (exploredConnection_ == null || 243 !exploredConnection_.getId().equalTo(event.getConnection().getId())) 244 { 245 exploreConnection(event.getConnection()); 246 } 247 activate(); 248 } 249 onConnectionUpdated(ConnectionUpdatedEvent event)250 public void onConnectionUpdated(ConnectionUpdatedEvent event) 251 { 252 if (exploredConnection_ == null) 253 return; 254 255 if (!exploredConnection_.getId().equalTo(event.getConnectionId())) 256 return; 257 258 display_.updateExploredConnection(event.getHint()); 259 } 260 onConnectionListChanged(ConnectionListChangedEvent event)261 public void onConnectionListChanged(ConnectionListChangedEvent event) 262 { 263 updateConnections(event.getConnectionList()); 264 } 265 onActiveConnectionsChanged(ActiveConnectionsChangedEvent event)266 public void onActiveConnectionsChanged(ActiveConnectionsChangedEvent event) 267 { 268 updateActiveConnections(event.getActiveConnections()); 269 } 270 showError(String errorMessage)271 private void showError(String errorMessage) 272 { 273 globalDisplay_.showErrorMessage("Error", errorMessage); 274 } 275 onNewConnection()276 public void onNewConnection() 277 { 278 // if r session busy, fail 279 if (commands_.interruptR().isEnabled()) { 280 showError( 281 "The R session is currently busy. Wait for completion or " + 282 "interrupt the current session and retry."); 283 return; 284 } 285 286 // check for updates 287 if (!installersUpdated_) { 288 installersUpdated_ = true; 289 server_.updateOdbcInstallers( 290 new DelayedProgressRequestCallback<ConnectionUpdateResult>( 291 "Checking for Updates...") { 292 293 @Override 294 public void onSuccess(ConnectionUpdateResult result) 295 { 296 installersWarning_ = result.getWarning(); 297 showWizard(); 298 } 299 300 @Override 301 public void onError(ServerError error) 302 { 303 Debug.logError(error); 304 globalDisplay_.showErrorMessage("Failed to check for updates", error.getMessage()); 305 } 306 } 307 ); 308 } 309 else { 310 showWizard(); 311 } 312 } 313 showWizard()314 private void showWizard() 315 { 316 server_.getNewConnectionContext( 317 new DelayedProgressRequestCallback<NewConnectionContext>("Preparing Connections...") { 318 319 @Override 320 protected void onSuccess(final NewConnectionContext context) 321 { 322 // show dialog 323 NewConnectionWizard newConnectionWizard = new NewConnectionWizard( 324 context, 325 new ProgressOperationWithInput<ConnectionOptions>() { 326 @Override 327 public void execute(ConnectionOptions result, 328 ProgressIndicator indicator) 329 { 330 indicator.onCompleted(); 331 332 eventBus_.fireEvent(new PerformConnectionEvent( 333 result.getConnectVia(), 334 result.getConnectCode()) 335 ); 336 } 337 }, 338 installersWarning_ 339 ); 340 341 newConnectionWizard.showModal(); 342 } 343 } 344 ); 345 } 346 347 @Override onPerformConnection(PerformConnectionEvent event)348 public void onPerformConnection(PerformConnectionEvent event) 349 { 350 String connectVia = event.getConnectVia(); 351 String connectCode = event.getConnectCode(); 352 353 if (connectVia == ConnectionOptions.CONNECT_COPY_TO_CLIPBOARD) 354 { 355 DomUtils.copyCodeToClipboard(connectCode); 356 } 357 else if (connectVia == ConnectionOptions.CONNECT_R_CONSOLE) 358 { 359 eventBus_.fireEvent( 360 new SendToConsoleEvent(connectCode, true)); 361 362 display_.showConnectionProgress("Connecting"); 363 } 364 else if (connectVia == ConnectionOptions.CONNECT_NEW_R_SCRIPT || 365 connectVia == ConnectionOptions.CONNECT_NEW_R_NOTEBOOK) 366 { 367 String type; 368 String code = connectCode; 369 SourcePosition cursorPosition = null; 370 if (connectVia == ConnectionOptions.CONNECT_NEW_R_SCRIPT) 371 { 372 type = NewDocumentWithCodeEvent.R_SCRIPT; 373 code = code + "\n\n"; 374 } 375 else 376 { 377 type = NewDocumentWithCodeEvent.R_NOTEBOOK; 378 int codeLength = code.split("\n").length; 379 code = "---\n" + 380 "title: \"R Notebook\"\n" + 381 "output: html_notebook\n" + 382 "---\n" + 383 "\n" + 384 "```{r setup, include=FALSE}\n" + 385 code + "\n" + 386 "```\n" + 387 "\n" + 388 "```{r}\n" + 389 "\n" + 390 "```\n"; 391 cursorPosition = SourcePosition.create(9 + codeLength, 0); 392 } 393 394 eventBus_.fireEvent( 395 new NewDocumentWithCodeEvent(type, code, cursorPosition, true)); 396 397 display_.showConnectionProgress("Connecting"); 398 } 399 } 400 401 @Override onViewConnectionDataset(ViewConnectionDatasetEvent event)402 public void onViewConnectionDataset(ViewConnectionDatasetEvent event) 403 { 404 if (exploredConnection_ == null) 405 return; 406 407 GlobalProgressDelayer progress = new GlobalProgressDelayer( 408 globalDisplay_, 100, "Previewing table..."); 409 410 server_.connectionPreviewObject( 411 exploredConnection_.getId(), 412 event.getDataset().createSpecifier(), 413 new VoidServerRequestCallback(progress.getIndicator())); 414 } 415 416 @Handler onRemoveConnection()417 public void onRemoveConnection() 418 { 419 if (exploredConnection_ == null) 420 return; 421 422 // protect the connection from interleaving actions while the dialog is up 423 final Connection removingConnection = exploredConnection_; 424 exploredConnection_ = null; 425 426 globalDisplay_.showYesNoMessage( 427 MessageDialog.QUESTION, 428 "Remove Connection", 429 "Are you sure you want to remove this connection from the connection history?", 430 false /* includeCancel */, 431 () -> { 432 server_.removeConnection( 433 removingConnection.getId(), 434 new VoidServerRequestCallback() 435 { 436 @Override 437 protected void onSuccess() 438 { 439 exploredConnection_ = removingConnection; 440 disconnectConnection(false); 441 showAllConnections(!userPrefs_.reducedMotion().getValue()); 442 } 443 @Override 444 protected void onFailure() 445 { 446 exploredConnection_ = removingConnection; 447 } 448 }); 449 }, 450 () -> { 451 // if user selects No, restore interleaving actions 452 exploredConnection_ = removingConnection; 453 }, 454 true /* yes is default */); 455 } 456 457 @Handler onDisconnectConnection()458 public void onDisconnectConnection() 459 { 460 disconnectConnection(true); 461 } 462 disconnectConnection(boolean prompt)463 private void disconnectConnection(boolean prompt) 464 { 465 if (exploredConnection_ == null) 466 return; 467 468 // define connect operation 469 final Operation connectOperation = new Operation() { 470 @Override 471 public void execute() 472 { 473 server_.connectionDisconnect(exploredConnection_.getId(), 474 new VoidServerRequestCallback()); 475 } 476 }; 477 478 if (prompt) 479 { 480 StringBuilder builder = new StringBuilder(); 481 builder.append("Are you sure you want to disconnect?"); 482 globalDisplay_.showYesNoMessage( 483 MessageDialog.QUESTION, 484 "Disconnect", 485 builder.toString(), 486 connectOperation, 487 true); 488 } 489 else 490 { 491 connectOperation.execute(); 492 } 493 } 494 495 496 @Handler onRefreshConnection()497 public void onRefreshConnection() 498 { 499 if (exploredConnection_ == null) 500 return; 501 502 display_.updateExploredConnection(""); 503 } 504 showAllConnections(boolean animate)505 private void showAllConnections(boolean animate) 506 { 507 exploredConnection_ = null; 508 display_.showConnectionsList(animate); 509 } 510 updateConnections(JsArray<Connection> connections)511 private void updateConnections(JsArray<Connection> connections) 512 { 513 // update all connections 514 allConnections_.clear(); 515 for (int i = 0; i<connections.length(); i++) 516 allConnections_.add(connections.get(i)); 517 518 // set filtered connections 519 display_.setConnections(filteredConnections()); 520 521 // update explored connection 522 if (exploredConnection_ != null) 523 { 524 for (int i = 0; i<connections.length(); i++) 525 { 526 if (connections.get(i).getId() == exploredConnection_.getId()) 527 { 528 exploredConnection_ = connections.get(i); 529 display_.setExploredConnection(exploredConnection_); 530 break; 531 } 532 } 533 } 534 } 535 updateActiveConnections(JsArray<ConnectionId> connections)536 private void updateActiveConnections(JsArray<ConnectionId> connections) 537 { 538 activeConnections_.clear(); 539 for (int i = 0; i<connections.length(); i++) 540 activeConnections_.add(connections.get(i)); 541 display_.setActiveConnections(activeConnections_); 542 manageUI(); 543 } 544 exploreConnection(Connection connection)545 private void exploreConnection(Connection connection) 546 { 547 exploredConnection_ = connection; 548 display_.showConnectionExplorer(connection, state_.connectVia().getValue()); 549 manageUI(); 550 } 551 manageUI()552 private void manageUI() 553 { 554 if (exploredConnection_ != null) 555 { 556 boolean connected = isConnected(exploredConnection_.getId()); 557 commands_.removeConnection().setVisible(!connected); 558 commands_.disconnectConnection().setVisible(connected); 559 // TODO: show connection actions 560 commands_.refreshConnection().setVisible(connected); 561 } 562 else 563 { 564 commands_.removeConnection().setVisible(false); 565 commands_.disconnectConnection().setVisible(false); 566 // TODO: hide connection actions 567 commands_.refreshConnection().setVisible(false); 568 } 569 } 570 isConnected(ConnectionId id)571 private boolean isConnected(ConnectionId id) 572 { 573 for (int i=0; i<activeConnections_.size(); i++) 574 if (activeConnections_.get(i).equalTo(id)) 575 return true; 576 return false; 577 } 578 filteredConnections()579 private List<Connection> filteredConnections() 580 { 581 String query = display_.getSearchFilter(); 582 final String[] splat = query.toLowerCase().split("\\s+"); 583 return ListUtil.filter(allConnections_, 584 new FilterPredicate<Connection>() 585 { 586 @Override 587 public boolean test(Connection connection) 588 { 589 for (String el : splat) 590 { 591 boolean match = 592 connection.getHost().toLowerCase().contains(el); 593 if (!match) 594 return false; 595 } 596 return true; 597 } 598 }); 599 } 600 601 private final GlobalDisplay globalDisplay_; 602 603 private final Display display_; 604 private final EventBus eventBus_; 605 private final Commands commands_; 606 private UserState state_; 607 private UserPrefs userPrefs_; 608 private final ConnectionsServerOperations server_; 609 @SuppressWarnings("unused") private final ApplicationInterrupt applicationInterrupt_; 610 611 // client state 612 public static final String MODULE_CONNECTIONS = "connections-pane"; 613 private static final String KEY_EXPLORED_CONNECTION = "exploredConnections"; 614 private Connection exploredConnection_; 615 private Connection lastExploredConnection_; 616 617 private ArrayList<Connection> allConnections_ = new ArrayList<>(); 618 private ArrayList<ConnectionId> activeConnections_ = new ArrayList<>(); 619 620 private static boolean installersUpdated_ = false; 621 private static String installersWarning_ = null; 622 } 623