1 /*
2  * PublishingPreferencesPane.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 
16 package org.rstudio.studio.client.workbench.prefs.views;
17 
18 import com.google.gwt.dom.client.Style.Unit;
19 import com.google.gwt.event.dom.client.ChangeEvent;
20 import com.google.gwt.event.dom.client.ChangeHandler;
21 import com.google.gwt.event.dom.client.ClickEvent;
22 import com.google.gwt.event.dom.client.ClickHandler;
23 import com.google.gwt.event.logical.shared.ValueChangeEvent;
24 import com.google.gwt.event.logical.shared.ValueChangeHandler;
25 import com.google.gwt.resources.client.ImageResource;
26 import com.google.gwt.user.client.ui.CheckBox;
27 import com.google.gwt.user.client.ui.HorizontalPanel;
28 import com.google.gwt.user.client.ui.Label;
29 import com.google.gwt.user.client.ui.VerticalPanel;
30 import com.google.inject.Inject;
31 
32 import org.rstudio.core.client.CommandWithArg;
33 import org.rstudio.core.client.ElementIds;
34 import org.rstudio.core.client.prefs.PreferencesDialogBaseResources;
35 import org.rstudio.core.client.prefs.RestartRequirement;
36 import org.rstudio.core.client.resources.ImageResource2x;
37 import org.rstudio.core.client.widget.FileChooserTextBox;
38 import org.rstudio.core.client.widget.Operation;
39 import org.rstudio.core.client.widget.OperationWithInput;
40 import org.rstudio.core.client.widget.ThemedButton;
41 import org.rstudio.studio.client.common.GlobalDisplay;
42 import org.rstudio.studio.client.common.HelpLink;
43 import org.rstudio.studio.client.common.dependencies.DependencyManager;
44 import org.rstudio.studio.client.rsconnect.RSConnect;
45 import org.rstudio.studio.client.rsconnect.model.RSConnectAccount;
46 import org.rstudio.studio.client.rsconnect.model.RSConnectServerOperations;
47 import org.rstudio.studio.client.rsconnect.ui.RSAccountConnector;
48 import org.rstudio.studio.client.rsconnect.ui.RSConnectAccountList;
49 import org.rstudio.studio.client.server.ServerError;
50 import org.rstudio.studio.client.server.ServerRequestCallback;
51 import org.rstudio.studio.client.server.Void;
52 import org.rstudio.studio.client.workbench.prefs.model.UserPrefs;
53 import org.rstudio.studio.client.workbench.prefs.model.UserState;
54 
55 public class PublishingPreferencesPane extends PreferencesPane
56 {
57    @Inject
PublishingPreferencesPane(GlobalDisplay globalDisplay, RSConnectServerOperations server, RSAccountConnector connector, UserPrefs prefs, UserState state, DependencyManager deps)58    public PublishingPreferencesPane(GlobalDisplay globalDisplay,
59                                     RSConnectServerOperations server,
60                                     RSAccountConnector connector,
61                                     UserPrefs prefs,
62                                     UserState state,
63                                     DependencyManager deps)
64    {
65       reloadRequired_ = false;
66       display_ = globalDisplay;
67       userPrefs_ = prefs;
68       userState_ = state;
69       server_ = server;
70       connector_ = connector;
71       deps_ = deps;
72 
73       VerticalPanel accountPanel = new VerticalPanel();
74       HorizontalPanel hpanel = new HorizontalPanel();
75 
76       String accountListLabel = "Publishing Accounts";
77       accountList_ = new RSConnectAccountList(server, globalDisplay, true, true, accountListLabel);
78       accountList_.setHeight("150px");
79       accountList_.setWidth("300px");
80       accountList_.getElement().getStyle().setMarginBottom(15, Unit.PX);
81       accountList_.getElement().getStyle().setMarginLeft(3, Unit.PX);
82       hpanel.add(accountList_);
83 
84       accountList_.setOnRefreshCompleted(new Operation()
85       {
86          @Override
87          public void execute()
88          {
89             setButtonEnabledState();
90          }
91       });
92       accountList_.addChangeHandler(new ChangeHandler()
93       {
94          @Override
95          public void onChange(ChangeEvent arg0)
96          {
97             setButtonEnabledState();
98          }
99       });
100 
101       VerticalPanel vpanel = new VerticalPanel();
102       hpanel.add(vpanel);
103 
104       connectButton_ = new ThemedButton("Connect...");
105       connectButton_.getElement().getStyle().setMarginBottom(5, Unit.PX);
106       connectButton_.setWidth("100%");
107       connectButton_.setWrapperWidth("100%");
108       ElementIds.assignElementId(connectButton_.getElement(), ElementIds.PUBLISH_CONNECT);
109       connectButton_.addClickHandler(new ClickHandler()
110       {
111          @Override
112          public void onClick(ClickEvent event)
113          {
114             onConnect();
115          }
116       });
117       vpanel.add(connectButton_);
118 
119       reconnectButton_ = new ThemedButton("Reconnect...");
120       reconnectButton_.getElement().getStyle().setMarginBottom(5, Unit.PX);
121       reconnectButton_.setWidth("100%");
122       reconnectButton_.setWrapperWidth("100%");
123       ElementIds.assignElementId(reconnectButton_.getElement(), ElementIds.PUBLISH_RECONNECT);
124       reconnectButton_.addClickHandler(new ClickHandler()
125       {
126          @Override
127          public void onClick(ClickEvent event)
128          {
129             onReconnect();
130          }
131       });
132       vpanel.add(reconnectButton_);
133 
134       disconnectButton_ = new ThemedButton("Disconnect");
135       disconnectButton_.setWidth("100%");
136       disconnectButton_.setWrapperWidth("100%");
137       ElementIds.assignElementId(disconnectButton_.getElement(), ElementIds.PUBLISH_DISCONNECT);
138       disconnectButton_.addClickHandler(new ClickHandler()
139       {
140          @Override
141          public void onClick(ClickEvent event)
142          {
143             onDisconnect();
144          }
145       });
146       vpanel.add(disconnectButton_);
147 
148       setButtonEnabledState();
149 
150       Label accountLabel = headerLabel(accountListLabel);
151       accountPanel.add(accountLabel);
152       accountPanel.add(hpanel);
153       add(accountPanel);
154 
155       // special UI to show when we detect that there are account records but
156       // the RSConnect package isn't installed
157       final VerticalPanel missingPkgPanel = new VerticalPanel();
158       missingPkgPanel.setVisible(false);
159       missingPkgPanel.add(new Label(
160             "Account records appear to exist, but cannot be viewed because a " +
161             "required package is not installed."));
162       ThemedButton installPkgs = new ThemedButton("Install Missing Packages");
163       installPkgs.addClickHandler(new ClickHandler()
164       {
165          @Override
166          public void onClick(ClickEvent arg0)
167          {
168             deps_.withRSConnect("Viewing publish accounts", false, null,
169                                 new CommandWithArg<Boolean>()
170             {
171                @Override
172                public void execute(Boolean succeeded)
173                {
174                   if (succeeded)
175                   {
176                      // refresh the account list to show the accounts
177                      accountList_.refreshAccountList();
178 
179                      // remove the "missing package" UI
180                      missingPkgPanel.setVisible(false);
181                   }
182                }
183             });
184          }
185       });
186       installPkgs.getElement().getStyle().setMarginLeft(0, Unit.PX);
187       installPkgs.getElement().getStyle().setMarginTop(10, Unit.PX);
188       missingPkgPanel.add(installPkgs);
189       missingPkgPanel.getElement().getStyle().setMarginBottom(20, Unit.PX);
190       add(missingPkgPanel);
191 
192       final CheckBox chkEnableRSConnect = checkboxPref("Enable publishing to RStudio Connect",
193             userState_.enableRsconnectPublishUi());
194       final HorizontalPanel rsconnectPanel = checkBoxWithHelp(chkEnableRSConnect,
195                                                         "rstudio_connect",
196                                                         "Information about RStudio Connect");
197       lessSpaced(rsconnectPanel);
198 
199       add(headerLabel("Settings"));
200       CheckBox chkEnablePublishing = checkboxPref("Enable publishing documents, apps, and APIs",
201             userState_.showPublishUi());
202       chkEnablePublishing.addValueChangeHandler(new ValueChangeHandler<Boolean>(){
203          @Override
204          public void onValueChange(ValueChangeEvent<Boolean> event)
205          {
206             reloadRequired_ = true;
207             rsconnectPanel.setVisible(
208                   RSConnect.showRSConnectUI() && event.getValue());
209          }
210       });
211       add(chkEnablePublishing);
212 
213       if (RSConnect.showRSConnectUI())
214          add(rsconnectPanel);
215 
216       add(checkboxPref("Show diagnostic information when publishing",
217             userPrefs_.showPublishDiagnostics()));
218 
219       add(spacedBefore(headerLabel("SSL Certificates")));
220 
221       add(checkboxPref("Check SSL certificates when publishing",
222             userPrefs_.publishCheckCertificates()));
223 
224       CheckBox useCaBundle = checkboxPref("Use custom CA bundle",
225             userPrefs_.usePublishCaBundle());
226       useCaBundle.addValueChangeHandler(
227             val -> caBundlePath_.setVisible(val.getValue()));
228       add(useCaBundle);
229 
230       caBundlePath_ = new FileChooserTextBox(
231          "", "(none)", ElementIds.TextBoxButtonId.CA_BUNDLE, false, null, null);
232       caBundlePath_.setText(userPrefs_.publishCaBundle().getValue());
233       caBundlePath_.setVisible(userPrefs_.usePublishCaBundle().getValue());
234       add(caBundlePath_);
235 
236       add(spacedBefore(new HelpLink("Troubleshooting Deployments",
237             "troubleshooting_deployments")));
238 
239       server_.hasOrphanedAccounts(new ServerRequestCallback<Double>()
240       {
241          @Override
242          public void onResponseReceived(Double numOrphans)
243          {
244             missingPkgPanel.setVisible(numOrphans > 0);
245          }
246 
247          @Override
248          public void onError(ServerError error)
249          {
250             // if we can't determine whether orphans exist, presume that they
251             // don't (this state is recoverable as we'll attempt to install
252             // rsconnect if necessary and refresh the account list when the user
253             // tries to interact with it)
254          }
255       });
256    }
257 
258    @Override
initialize(UserPrefs rPrefs)259    protected void initialize(UserPrefs rPrefs)
260    {
261    }
262 
263    @Override
onApply(UserPrefs rPrefs)264    public RestartRequirement onApply(UserPrefs rPrefs)
265    {
266       RestartRequirement restartRequirement = super.onApply(rPrefs);
267 
268       if (reloadRequired_)
269          restartRequirement.setUiReloadRequired(true);
270 
271       userPrefs_.publishCaBundle().setGlobalValue(caBundlePath_.getText());
272 
273       return restartRequirement;
274    }
275 
276    @Override
getIcon()277    public ImageResource getIcon()
278    {
279       return new ImageResource2x(PreferencesDialogBaseResources.INSTANCE.iconPublishing2x());
280    }
281 
282    @Override
validate()283    public boolean validate()
284    {
285       return true;
286    }
287 
288    @Override
getName()289    public String getName()
290    {
291       return "Publishing";
292    }
293 
onDisconnect()294    private void onDisconnect()
295    {
296       final RSConnectAccount account = accountList_.getSelectedAccount();
297       if (account == null)
298       {
299          display_.showErrorMessage("Error Disconnecting Account",
300                "Please select an account to disconnect.");
301          return;
302       }
303       display_.showYesNoMessage(
304             GlobalDisplay.MSG_QUESTION,
305             "Confirm Remove Account",
306             "Are you sure you want to disconnect the '" +
307               account.getName() +
308             "' account on '" +
309               account.getServer() + "'" +
310             "? This won't delete the account on the server.",
311             false,
312             new Operation()
313             {
314                @Override
315                public void execute()
316                {
317                   onConfirmDisconnect(account);
318                }
319             }, null, null, "Disconnect Account", "Cancel", false);
320    }
321 
onConfirmDisconnect(final RSConnectAccount account)322    private void onConfirmDisconnect(final RSConnectAccount account)
323    {
324       server_.removeRSConnectAccount(account.getName(),
325             account.getServer(), new ServerRequestCallback<Void>()
326       {
327          @Override
328          public void onResponseReceived(Void v)
329          {
330             accountList_.refreshAccountList();
331          }
332 
333          @Override
334          public void onError(ServerError error)
335          {
336             display_.showErrorMessage("Error Disconnecting Account",
337                                       error.getMessage());
338          }
339       });
340    }
341 
onConnect()342    private void onConnect()
343    {
344       // if there's already at least one account connected, the requisite
345       // packages must be installed
346       if (accountList_.getAccountCount() > 0)
347       {
348          showAccountWizard();
349       }
350       else
351       {
352          deps_.withRSConnect("Connecting a publishing account", false, null,
353                              new CommandWithArg<Boolean>()
354          {
355             @Override
356             public void execute(Boolean succeeded)
357             {
358                // refresh the account list in case there are accounts already on
359                // the system (e.g. package was installed at one point and some
360                // metadata remains)
361                accountList_.refreshAccountList();
362 
363                showAccountWizard();
364             }
365          });
366       }
367    }
368 
onReconnect()369    private void onReconnect()
370    {
371       connector_.showReconnectWizard(accountList_.getSelectedAccount(),
372             new OperationWithInput<Boolean>()
373       {
374          @Override
375          public void execute(Boolean successful)
376          {
377             if (successful)
378             {
379                accountList_.refreshAccountList();
380             }
381          }
382       });
383    }
384 
showAccountWizard()385    private void showAccountWizard()
386    {
387       connector_.showAccountWizard(false, true,
388             new OperationWithInput<Boolean>()
389       {
390          @Override
391          public void execute(Boolean successful)
392          {
393             if (successful)
394             {
395                accountList_.refreshAccountList();
396             }
397          }
398       });
399    }
400 
setButtonEnabledState()401    private void setButtonEnabledState()
402    {
403       disconnectButton_.setEnabled(
404             accountList_.getSelectedAccount() != null);
405 
406       reconnectButton_.setEnabled(
407             accountList_.getSelectedAccount() != null &&
408             !accountList_.getSelectedAccount().isCloudAccount());
409    }
410 
411    private final GlobalDisplay display_;
412    private final UserPrefs userPrefs_;
413    private final UserState userState_;
414    private final RSConnectServerOperations server_;
415    private final RSAccountConnector connector_;
416    private final DependencyManager deps_;
417 
418    private RSConnectAccountList accountList_;
419    private ThemedButton connectButton_;
420    private ThemedButton disconnectButton_;
421    private ThemedButton reconnectButton_;
422    private FileChooserTextBox caBundlePath_;
423    private boolean reloadRequired_;
424 }
425 
426