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