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