1 /* 2 * RSAccountConnector.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.rsconnect.ui; 16 17 import org.rstudio.core.client.command.CommandBinder; 18 import org.rstudio.core.client.command.Handler; 19 import org.rstudio.core.client.widget.Operation; 20 import org.rstudio.core.client.widget.OperationWithInput; 21 import org.rstudio.core.client.widget.ProgressOperationWithInput; 22 import org.rstudio.core.client.widget.ProgressIndicator; 23 import org.rstudio.studio.client.application.events.EventBus; 24 import org.rstudio.studio.client.common.GlobalDisplay; 25 import org.rstudio.studio.client.common.satellite.Satellite; 26 import org.rstudio.studio.client.rsconnect.events.EnableRStudioConnectUIEvent; 27 import org.rstudio.studio.client.rsconnect.model.NewRSConnectAccountResult; 28 import org.rstudio.studio.client.rsconnect.model.NewRSConnectAccountResult.AccountType; 29 import org.rstudio.studio.client.rsconnect.model.RSConnectAccount; 30 import org.rstudio.studio.client.rsconnect.model.RSConnectAuthUser; 31 import org.rstudio.studio.client.rsconnect.model.RSConnectPreAuthToken; 32 import org.rstudio.studio.client.rsconnect.model.RSConnectServerEntry; 33 import org.rstudio.studio.client.rsconnect.model.RSConnectServerInfo; 34 import org.rstudio.studio.client.rsconnect.model.RSConnectServerOperations; 35 import org.rstudio.studio.client.server.ServerError; 36 import org.rstudio.studio.client.server.ServerRequestCallback; 37 import org.rstudio.studio.client.server.Void; 38 import org.rstudio.studio.client.workbench.commands.Commands; 39 import org.rstudio.studio.client.workbench.model.Session; 40 import org.rstudio.studio.client.workbench.model.SessionUtils; 41 import org.rstudio.studio.client.workbench.prefs.model.UserPrefs; 42 import org.rstudio.studio.client.workbench.prefs.model.UserState; 43 import org.rstudio.studio.client.workbench.prefs.views.PublishingPreferencesPane; 44 import org.rstudio.studio.client.workbench.ui.OptionsLoader; 45 46 import com.google.gwt.core.client.JsArray; 47 import com.google.gwt.event.logical.shared.CloseEvent; 48 import com.google.gwt.event.logical.shared.CloseHandler; 49 import com.google.gwt.user.client.ui.PopupPanel; 50 import com.google.inject.Inject; 51 import com.google.inject.Provider; 52 import com.google.inject.Singleton; 53 54 @Singleton 55 public class RSAccountConnector implements EnableRStudioConnectUIEvent.Handler 56 { 57 public interface Binder 58 extends CommandBinder<Commands, RSAccountConnector> {} 59 60 // possible results of attempting to connect an account 61 enum AccountConnectResult 62 { 63 Incomplete, 64 Successful, 65 Failed 66 } 67 68 @Inject RSAccountConnector(RSConnectServerOperations server, GlobalDisplay display, Commands commands, Binder binder, OptionsLoader.Shim optionsLoader, EventBus events, Session session, Provider<UserPrefs> pUiPrefs, Provider<UserState> pState, Satellite satellite)69 public RSAccountConnector(RSConnectServerOperations server, 70 GlobalDisplay display, 71 Commands commands, 72 Binder binder, 73 OptionsLoader.Shim optionsLoader, 74 EventBus events, 75 Session session, 76 Provider<UserPrefs> pUiPrefs, 77 Provider<UserState> pState, 78 Satellite satellite) 79 { 80 server_ = server; 81 display_ = display; 82 optionsLoader_ = optionsLoader; 83 pUserPrefs_ = pUiPrefs; 84 pUserState_ = pState; 85 session_ = session; 86 87 events.addHandler(EnableRStudioConnectUIEvent.TYPE, this); 88 89 binder.bind(commands, this); 90 91 // register satellite callback 92 if (!Satellite.isCurrentWindowSatellite()) 93 exportManageAccountsCallback(); 94 } 95 showAccountWizard( boolean forFirstAccount, boolean withCloudOption, final OperationWithInput<Boolean> onCompleted)96 public void showAccountWizard( 97 boolean forFirstAccount, 98 boolean withCloudOption, 99 final OperationWithInput<Boolean> onCompleted) 100 { 101 if (pUserState_.get().enableRsconnectPublishUi().getGlobalValue()) 102 { 103 showAccountTypeWizard(forFirstAccount, withCloudOption, onCompleted); 104 } 105 else 106 { 107 showShinyAppsDialog(onCompleted); 108 } 109 } 110 showReconnectWizard( final RSConnectAccount account, final OperationWithInput<Boolean> onCompleted)111 public void showReconnectWizard( 112 final RSConnectAccount account, 113 final OperationWithInput<Boolean> onCompleted) 114 { 115 server_.getServerUrls(new ServerRequestCallback<JsArray<RSConnectServerEntry>>() 116 { 117 @Override 118 public void onResponseReceived(JsArray<RSConnectServerEntry> entries) 119 { 120 boolean found = false; 121 for (int i = 0; i < entries.length(); i++) 122 { 123 if (entries.get(i).getName().equalsIgnoreCase( 124 account.getServer())) 125 { 126 RSConnectReconnectWizard wizard = 127 new RSConnectReconnectWizard( 128 server_, 129 display_, 130 account, 131 entries.get(i).getUrl(), 132 new ProgressOperationWithInput<NewRSConnectAccountResult>() 133 { 134 @Override 135 public void execute(NewRSConnectAccountResult input, 136 final ProgressIndicator indicator) 137 { 138 processDialogResult(input, indicator, onCompleted); 139 } 140 }); 141 wizard.showModal(); 142 found = true; 143 break; 144 } 145 } 146 147 if (!found) 148 { 149 display_.showErrorMessage("Server Information Not Found", 150 "RStudio could not retrieve server information for " + 151 "the selected account."); 152 } 153 } 154 155 @Override 156 public void onError(ServerError error) 157 { 158 display_.showErrorMessage("Can't Find Servers", 159 "RStudio could not retrieve server information."); 160 } 161 }); 162 } 163 164 @Handler onRsconnectManageAccounts()165 public void onRsconnectManageAccounts() 166 { 167 if (Satellite.isCurrentWindowSatellite()) 168 { 169 callSatelliteManageAccounts(); 170 } 171 else 172 { 173 optionsLoader_.showOptions(PublishingPreferencesPane.class, true); 174 } 175 } 176 177 // Event handlers --------------------------------------------------------- 178 179 @Override onEnableRStudioConnectUI(EnableRStudioConnectUIEvent event)180 public void onEnableRStudioConnectUI(EnableRStudioConnectUIEvent event) 181 { 182 pUserState_.get().enableRsconnectPublishUi().setGlobalValue(event.getEnable()); 183 pUserState_.get().writeState(); 184 } 185 186 // Private methods -------------------------------------------------------- 187 showShinyAppsDialog( final OperationWithInput<Boolean> onCompleted)188 private void showShinyAppsDialog( 189 final OperationWithInput<Boolean> onCompleted) 190 { 191 RSConnectCloudDialog dialog = new RSConnectCloudDialog( 192 new ProgressOperationWithInput<NewRSConnectAccountResult>() 193 { 194 @Override 195 public void execute(NewRSConnectAccountResult input, 196 ProgressIndicator indicator) 197 { 198 processDialogResult(input, indicator, onCompleted); 199 } 200 }, 201 new Operation() 202 { 203 @Override 204 public void execute() 205 { 206 onCompleted.execute(false); 207 } 208 }); 209 dialog.showModal(); 210 } 211 showAccountTypeWizard( boolean forFirstAccount, boolean withCloudOption, final OperationWithInput<Boolean> onCompleted)212 private void showAccountTypeWizard( 213 boolean forFirstAccount, 214 boolean withCloudOption, 215 final OperationWithInput<Boolean> onCompleted) 216 { 217 // ignore if wizard is already up 218 if (showingWizard_) 219 return; 220 221 RSConnectAccountWizard wizard = new RSConnectAccountWizard( 222 server_, 223 display_, 224 forFirstAccount, 225 withCloudOption && 226 SessionUtils.showExternalPublishUi(session_, pUserState_.get()), 227 new ProgressOperationWithInput<NewRSConnectAccountResult>() 228 { 229 @Override 230 public void execute(NewRSConnectAccountResult input, 231 final ProgressIndicator indicator) 232 { 233 processDialogResult(input, indicator, onCompleted); 234 } 235 }); 236 wizard.setGlassEnabled(true); 237 wizard.showModal(); 238 239 // remember whether wizard is showing 240 showingWizard_ = true; 241 wizard.addCloseHandler(new CloseHandler<PopupPanel>() 242 { 243 @Override 244 public void onClose(CloseEvent<PopupPanel> arg0) 245 { 246 showingWizard_ = false; 247 } 248 }); 249 } 250 processDialogResult(final NewRSConnectAccountResult input, final ProgressIndicator indicator, final OperationWithInput<Boolean> onCompleted)251 private void processDialogResult(final NewRSConnectAccountResult input, 252 final ProgressIndicator indicator, 253 final OperationWithInput<Boolean> onCompleted) 254 { 255 connectNewAccount(input, indicator, 256 new OperationWithInput<AccountConnectResult>() 257 { 258 @Override 259 public void execute(AccountConnectResult input) 260 { 261 if (input == AccountConnectResult.Failed) 262 { 263 // the connection failed--take down the dialog entirely 264 // (we do this when retrying doesn't make sense) 265 onCompleted.execute(false); 266 indicator.onCompleted(); 267 } 268 else if (input == AccountConnectResult.Incomplete) 269 { 270 // the connection didn't finish--take down the progress and 271 // allow retry 272 indicator.clearProgress(); 273 } 274 else if (input == AccountConnectResult.Successful) 275 { 276 // successful account connection--mark finished 277 onCompleted.execute(true); 278 indicator.onCompleted(); 279 } 280 } 281 }); 282 } 283 connectNewAccount( NewRSConnectAccountResult result, ProgressIndicator indicator, OperationWithInput<AccountConnectResult> onConnected)284 private void connectNewAccount( 285 NewRSConnectAccountResult result, 286 ProgressIndicator indicator, 287 OperationWithInput<AccountConnectResult> onConnected) 288 { 289 if (result.getAccountType() == AccountType.RSConnectCloudAccount) 290 { 291 connectCloudAccount(result, indicator, onConnected); 292 } 293 else 294 { 295 connectLocalAccount(result, indicator, onConnected); 296 } 297 } 298 connectCloudAccount( final NewRSConnectAccountResult result, final ProgressIndicator indicator, final OperationWithInput<AccountConnectResult> onConnected)299 private void connectCloudAccount( 300 final NewRSConnectAccountResult result, 301 final ProgressIndicator indicator, 302 final OperationWithInput<AccountConnectResult> onConnected) 303 { 304 // get command and substitute rsconnect for shinyapps 305 final String cmd = result.getCloudSecret().replace("shinyapps::", 306 "rsconnect::"); 307 if (!cmd.startsWith("rsconnect::setAccountInfo")) 308 { 309 display_.showErrorMessage("Error Connecting Account", 310 "The pasted command should start with " + 311 "rsconnect::setAccountInfo. If you're having trouble, try " + 312 "connecting your account manually; type " + 313 "?rsconnect::setAccountInfo at the R console for help."); 314 onConnected.execute(AccountConnectResult.Incomplete); 315 } 316 indicator.onProgress("Connecting account..."); 317 server_.connectRSConnectAccount(cmd, 318 new ServerRequestCallback<Void>() 319 { 320 @Override 321 public void onResponseReceived(Void v) 322 { 323 onConnected.execute(AccountConnectResult.Successful); 324 } 325 326 @Override 327 public void onError(ServerError error) 328 { 329 display_.showErrorMessage("Error Connecting Account", 330 "The command '" + cmd + "' failed. You can set up an " + 331 "account manually by using rsconnect::setAccountInfo; " + 332 "type ?rsconnect::setAccountInfo at the R console for " + 333 "more information."); 334 onConnected.execute(AccountConnectResult.Failed); 335 } 336 }); 337 } 338 connectLocalAccount( final NewRSConnectAccountResult result, final ProgressIndicator indicator, final OperationWithInput<AccountConnectResult> onConnected)339 private void connectLocalAccount( 340 final NewRSConnectAccountResult result, 341 final ProgressIndicator indicator, 342 final OperationWithInput<AccountConnectResult> onConnected) 343 344 { 345 indicator.onProgress("Adding account..."); 346 final RSConnectAuthUser user = result.getAuthUser(); 347 final RSConnectServerInfo serverInfo = result.getServerInfo(); 348 final RSConnectPreAuthToken token = result.getPreAuthToken(); 349 350 server_.registerUserToken(serverInfo.getName(), 351 result.getAccountNickname(), 352 user.getId(), token, new ServerRequestCallback<Void>() 353 { 354 @Override 355 public void onResponseReceived(Void result) 356 { 357 onConnected.execute(AccountConnectResult.Successful); 358 } 359 360 @Override 361 public void onError(ServerError error) 362 { 363 display_.showErrorMessage("Account Connect Failed", 364 "Your account was authenticated successfully, but could " + 365 "not be connected to RStudio. Make sure your installation " + 366 "of the 'rsconnect' package is correct for the server " + 367 "you're connecting to.\n\n" + 368 serverInfo.getInfoString() + "\n" + 369 error.getMessage()); 370 onConnected.execute(AccountConnectResult.Failed); 371 } 372 }); 373 } 374 exportManageAccountsCallback()375 private final native void exportManageAccountsCallback()/*-{ 376 var rsAccount = this; 377 $wnd.rsManageAccountsFromRStudioSatellite = $entry( 378 function() { 379 rsAccount.@org.rstudio.studio.client.rsconnect.ui.RSAccountConnector::onRsconnectManageAccounts()(); 380 } 381 ); 382 }-*/; 383 callSatelliteManageAccounts()384 private final native void callSatelliteManageAccounts()/*-{ 385 $wnd.opener.rsManageAccountsFromRStudioSatellite(); 386 }-*/; 387 388 private final GlobalDisplay display_; 389 private final RSConnectServerOperations server_; 390 private final OptionsLoader.Shim optionsLoader_; 391 private final Provider<UserPrefs> pUserPrefs_; 392 private final Provider<UserState> pUserState_; 393 private final Session session_; 394 395 private boolean showingWizard_; 396 } 397