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