1 /* 2 * RSConnect.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; 16 17 import java.util.ArrayList; 18 import java.util.List; 19 20 import com.google.gwt.aria.client.Roles; 21 import org.rstudio.core.client.CommandWithArg; 22 import org.rstudio.core.client.JsArrayUtil; 23 import org.rstudio.core.client.StringUtil; 24 import org.rstudio.core.client.command.CommandBinder; 25 import org.rstudio.core.client.dom.WindowEx; 26 import org.rstudio.core.client.files.FileSystemItem; 27 import org.rstudio.core.client.js.JsObject; 28 import org.rstudio.core.client.resources.ImageResource2x; 29 import org.rstudio.core.client.widget.ModalDialogBase; 30 import org.rstudio.core.client.widget.ModalDialogTracker; 31 import org.rstudio.core.client.widget.ProgressIndicator; 32 import org.rstudio.core.client.widget.ProgressOperation; 33 import org.rstudio.core.client.widget.ProgressOperationWithInput; 34 import org.rstudio.core.client.widget.ThemedButton; 35 import org.rstudio.core.client.widget.images.MessageDialogImages; 36 import org.rstudio.studio.client.application.Desktop; 37 import org.rstudio.studio.client.application.events.EventBus; 38 import org.rstudio.studio.client.common.FilePathUtils; 39 import org.rstudio.studio.client.common.GlobalDisplay; 40 import org.rstudio.studio.client.common.dependencies.DependencyManager; 41 import org.rstudio.studio.client.common.rpubs.RPubsUploader; 42 import org.rstudio.studio.client.common.rpubs.model.RPubsServerOperations; 43 import org.rstudio.studio.client.common.rpubs.ui.RPubsUploadDialog; 44 import org.rstudio.studio.client.common.satellite.Satellite; 45 import org.rstudio.studio.client.quarto.model.QuartoConfig; 46 import org.rstudio.studio.client.rsconnect.events.RSConnectActionEvent; 47 import org.rstudio.studio.client.rsconnect.events.RSConnectDeployInitiatedEvent; 48 import org.rstudio.studio.client.rsconnect.events.RSConnectDeploymentCancelledEvent; 49 import org.rstudio.studio.client.rsconnect.events.RSConnectDeploymentCompletedEvent; 50 import org.rstudio.studio.client.rsconnect.events.RSConnectDeploymentFailedEvent; 51 import org.rstudio.studio.client.rsconnect.events.RSConnectDeploymentStartedEvent; 52 import org.rstudio.studio.client.rsconnect.model.PlotPublishMRUList; 53 import org.rstudio.studio.client.rsconnect.model.QmdPublishDetails; 54 import org.rstudio.studio.client.rsconnect.model.RSConnectApplicationInfo; 55 import org.rstudio.studio.client.rsconnect.model.RSConnectDeploymentRecord; 56 import org.rstudio.studio.client.rsconnect.model.RSConnectDirectoryState; 57 import org.rstudio.studio.client.rsconnect.model.RSConnectLintResults; 58 import org.rstudio.studio.client.rsconnect.model.RSConnectPublishInput; 59 import org.rstudio.studio.client.rsconnect.model.RSConnectPublishResult; 60 import org.rstudio.studio.client.rsconnect.model.RSConnectPublishSettings; 61 import org.rstudio.studio.client.rsconnect.model.RSConnectPublishSource; 62 import org.rstudio.studio.client.rsconnect.model.RSConnectServerOperations; 63 import org.rstudio.studio.client.rsconnect.model.RenderedDocPreview; 64 import org.rstudio.studio.client.rsconnect.model.RmdPublishDetails; 65 import org.rstudio.studio.client.rsconnect.ui.RSAccountConnector; 66 import org.rstudio.studio.client.rsconnect.ui.RSConnectDeployDialog; 67 import org.rstudio.studio.client.rsconnect.ui.RSConnectPublishWizard; 68 import org.rstudio.studio.client.server.ServerError; 69 import org.rstudio.studio.client.server.ServerRequestCallback; 70 import org.rstudio.studio.client.workbench.commands.Commands; 71 import org.rstudio.studio.client.workbench.events.SessionInitEvent; 72 import org.rstudio.studio.client.workbench.model.ClientState; 73 import org.rstudio.studio.client.workbench.model.Session; 74 import org.rstudio.studio.client.workbench.model.SessionUtils; 75 import org.rstudio.studio.client.workbench.model.helper.JSObjectStateValue; 76 import org.rstudio.studio.client.workbench.prefs.model.UserPrefs; 77 import org.rstudio.studio.client.workbench.prefs.model.UserState; 78 import org.rstudio.studio.client.workbench.views.source.model.SourceServerOperations; 79 80 import com.google.gwt.core.client.JavaScriptObject; 81 import com.google.gwt.core.client.JsArray; 82 import com.google.gwt.core.client.JsArrayString; 83 import com.google.gwt.dom.client.Style.Unit; 84 import com.google.gwt.event.dom.client.ClickEvent; 85 import com.google.gwt.event.dom.client.ClickHandler; 86 import com.google.gwt.user.client.ui.HTML; 87 import com.google.gwt.user.client.ui.HorizontalPanel; 88 import com.google.gwt.user.client.ui.Image; 89 import com.google.gwt.user.client.ui.Widget; 90 import com.google.inject.Inject; 91 import com.google.inject.Provider; 92 import com.google.inject.Singleton; 93 94 @Singleton 95 public class RSConnect implements SessionInitEvent.Handler, 96 RSConnectActionEvent.Handler, 97 RSConnectDeployInitiatedEvent.Handler, 98 RSConnectDeploymentCompletedEvent.Handler, 99 RSConnectDeploymentFailedEvent.Handler, 100 RSConnectDeploymentCancelledEvent.Handler 101 { 102 public interface Binder 103 extends CommandBinder<Commands, RSConnect> {} 104 105 @Inject RSConnect(EventBus events, Commands commands, Session session, GlobalDisplay display, DependencyManager dependencyManager, Binder binder, RSConnectServerOperations server, SourceServerOperations sourceServer, RPubsServerOperations rpubsServer, RSAccountConnector connector, Provider<UserPrefs> pUserPrefs, Provider<UserState> pUserState, PlotPublishMRUList plotMru)106 public RSConnect(EventBus events, 107 Commands commands, 108 Session session, 109 GlobalDisplay display, 110 DependencyManager dependencyManager, 111 Binder binder, 112 RSConnectServerOperations server, 113 SourceServerOperations sourceServer, 114 RPubsServerOperations rpubsServer, 115 RSAccountConnector connector, 116 Provider<UserPrefs> pUserPrefs, 117 Provider<UserState> pUserState, 118 PlotPublishMRUList plotMru) 119 { 120 commands_ = commands; 121 display_ = display; 122 dependencyManager_ = dependencyManager; 123 session_ = session; 124 server_ = server; 125 sourceServer_ = sourceServer; 126 rpubsServer_ = rpubsServer; 127 events_ = events; 128 connector_ = connector; 129 pUserPrefs_ = pUserPrefs; 130 pUserState_ = pUserState; 131 plotMru_ = plotMru; 132 133 binder.bind(commands, this); 134 135 events.addHandler(SessionInitEvent.TYPE, this); 136 events.addHandler(RSConnectActionEvent.TYPE, this); 137 events.addHandler(RSConnectDeployInitiatedEvent.TYPE, this); 138 events.addHandler(RSConnectDeploymentCompletedEvent.TYPE, this); 139 events.addHandler(RSConnectDeploymentFailedEvent.TYPE, this); 140 events.addHandler(RSConnectDeploymentCancelledEvent.TYPE, this); 141 142 // satellite windows don't get session init events, so initialize the 143 // session here 144 if (Satellite.isCurrentWindowSatellite()) 145 { 146 ensureSessionInit(); 147 } 148 149 exportNativeCallbacks(); 150 } 151 152 @Override onSessionInit(SessionInitEvent sie)153 public void onSessionInit(SessionInitEvent sie) 154 { 155 ensureSessionInit(); 156 } 157 158 @Override onRSConnectAction(final RSConnectActionEvent event)159 public void onRSConnectAction(final RSConnectActionEvent event) 160 { 161 // ignore if we're already waiting for a dependency check 162 if (depsPending_) 163 return; 164 165 // see if we have the requisite R packages 166 depsPending_ = true; 167 dependencyManager_.withRSConnect( 168 "Publishing content", 169 event.getContentType() == CONTENT_TYPE_DOCUMENT || 170 event.getContentType() == CONTENT_TYPE_WEBSITE || 171 event.getContentType() == CONTENT_TYPE_QUARTO_WEBSITE , 172 null, new CommandWithArg<Boolean>() { 173 @Override 174 public void execute(Boolean succeeded) 175 { 176 if (succeeded) 177 handleRSConnectAction(event); 178 179 depsPending_ = false; 180 } 181 }); 182 } 183 supportedRPubsDocExtension(String filename)184 private boolean supportedRPubsDocExtension(String filename) 185 { 186 if (StringUtil.isNullOrEmpty(filename)) 187 return false; 188 189 String extension = FileSystemItem.getExtensionFromPath(filename).toLowerCase(); 190 return StringUtil.equals(extension, ".html") || 191 StringUtil.equals(extension, ".htm") || 192 StringUtil.equals(extension, ".nb.html"); 193 } 194 publishAsRPubs(RSConnectActionEvent event)195 private void publishAsRPubs(RSConnectActionEvent event) 196 { 197 // If previously published but the rendered file is now missing, give a warning instead 198 // of trying to republish. 199 if (event.getFromPrevious() != null && 200 !StringUtil.isNullOrEmpty(event.getFromPrevious().getBundleId()) && 201 StringUtil.isNullOrEmpty(event.getHtmlFile())) 202 { 203 display_.showErrorMessage("Republish Document", 204 "Only rendered documents can be republished to RPubs. " + 205 "To republish this document, click Knit or Preview to render it to HTML, then " + 206 "click the Republish button above the rendered document."); 207 return; 208 } 209 210 // If we don't have an html file, can't publish to RPubs, e.g. create a generic markdown 211 // file (.md), don't preview it, and try to publish it to RPubs. Also, prevent publishing 212 // unsupported output formats; relatively easy to get in this state; e.g. Knit and 213 // publish HTML to RPubs, then Knit as PDF and try to republish. 214 if (StringUtil.isNullOrEmpty(event.getHtmlFile()) || 215 (event.getContentType() == CONTENT_TYPE_DOCUMENT && 216 !supportedRPubsDocExtension(event.getHtmlFile()))) 217 { 218 showUnsupportedRPubsFormatMessage(); 219 return; 220 } 221 222 String ctx = "Publish " + contentTypeDesc(event.getContentType()); 223 RPubsUploadDialog dlg = new RPubsUploadDialog( 224 "Publish Wizard", 225 ctx, 226 event.getFromPreview() != null ? 227 event.getFromPreview().getSourceFile() : null, 228 event.getHtmlFile(), 229 event.getFromPrevious() == null ? 230 "" : event.getFromPrevious().getBundleId(), 231 false); 232 dlg.showModal(); 233 } 234 showPublishUI(final RSConnectActionEvent event)235 private void showPublishUI(final RSConnectActionEvent event) 236 { 237 final RSConnectPublishInput input = new RSConnectPublishInput(event); 238 239 // set these inside the wizard input so we don't need to pass around 240 // session/prefs 241 input.setConnectUIEnabled( 242 pUserState_.get().enableRsconnectPublishUi().getGlobalValue()); 243 input.setExternalUIEnabled( 244 session_.getSessionInfo().getAllowExternalPublish()); 245 input.setDescription(event.getDescription()); 246 247 if (event.getFromPrevious() != null) 248 { 249 switch (event.getContentType()) 250 { 251 case CONTENT_TYPE_APP: 252 case CONTENT_TYPE_APP_SINGLE: 253 publishAsCode(event, null, true); 254 break; 255 case CONTENT_TYPE_PRES: 256 case CONTENT_TYPE_PLOT: 257 case CONTENT_TYPE_HTML: 258 case CONTENT_TYPE_DOCUMENT: 259 case CONTENT_TYPE_WEBSITE: 260 if (event.getFromPrevious().getServer() == "rpubs.com") 261 { 262 publishAsRPubs(event); 263 } 264 else 265 { 266 fillInputFromDoc(input, event.getPath(), 267 new CommandWithArg<RSConnectPublishInput>() 268 { 269 @Override 270 public void execute(RSConnectPublishInput arg) 271 { 272 if (arg == null) 273 return; 274 275 boolean isQuarto = false; 276 if (event.getFromPreview() != null) 277 { 278 isQuarto = event.getFromPreview().isQuarto(); 279 } 280 281 if (event.getFromPrevious().getAsStatic()) 282 publishAsFiles(event, 283 new RSConnectPublishSource(event.getPath(), 284 event.getHtmlFile(), 285 arg.getWebsiteDir(), 286 arg.getWebsiteOutputDir(), 287 arg.isSelfContained(), 288 true, 289 arg.isShiny(), 290 isQuarto, 291 arg.getDescription(), 292 event.getContentType())); 293 else 294 publishAsCode(event, arg.getWebsiteDir(), 295 arg.isShiny()); 296 } 297 }); 298 } 299 break; 300 case CONTENT_TYPE_QUARTO_WEBSITE: 301 // Quarto website publishing metadata is extracted from the active Quarto project 302 QuartoConfig config = session_.getSessionInfo().getQuartoConfig(); 303 FileSystemItem projectDir = FileSystemItem.createDir(config.project_dir); 304 String websiteOutputDir = projectDir.completePath(config.project_output_dir); 305 306 if (event.getFromPrevious().getAsStatic()) 307 { 308 publishAsFiles(event, 309 new RSConnectPublishSource(event.getPath(), 310 config.project_dir, 311 config.project_dir, 312 websiteOutputDir, 313 input.isSelfContained(), 314 true, // isStatic 315 false, // isShiny 316 true, // isQuarto 317 input.getDescription(), 318 event.getContentType())); 319 } 320 else 321 { 322 publishAsCode(event, config.project_dir, false /* isShiny */); 323 } 324 break; 325 326 327 case CONTENT_TYPE_PLUMBER_API: 328 publishAsCode(event, null, false); 329 break; 330 } 331 } 332 else 333 { 334 // plots and HTML are implicitly self-contained 335 if (event.getContentType() == CONTENT_TYPE_PLOT || 336 event.getContentType() == CONTENT_TYPE_HTML || 337 event.getContentType() == CONTENT_TYPE_PRES) 338 { 339 input.setIsSelfContained(true); 340 } 341 342 // if R Markdown, get info on what we're publishing from the server 343 if (event.getFromPreview() != null) 344 { 345 input.setSourceRmd(FileSystemItem.createFile( 346 event.getFromPreview().getSourceFile())); 347 fillInputFromDoc(input, event.getFromPreview().getSourceFile(), 348 new CommandWithArg<RSConnectPublishInput>() 349 { 350 @Override 351 public void execute(RSConnectPublishInput arg) 352 { 353 showPublishUI(arg); 354 } 355 }); 356 } 357 else if (event.getContentType() == RSConnect.CONTENT_TYPE_QUARTO_WEBSITE) 358 { 359 QuartoConfig config = session_.getSessionInfo().getQuartoConfig(); 360 FileSystemItem projectDir = FileSystemItem.createDir(config.project_dir); 361 362 // fill publish input from session 363 input.setIsQuarto(true); 364 input.setWebsiteDir(config.project_dir); 365 input.setWebsiteOutputDir(projectDir.completePath(config.project_output_dir)); 366 showPublishUI(input); 367 } 368 else 369 { 370 showPublishUI(input); 371 } 372 } 373 } 374 showPublishUI(RSConnectPublishInput input)375 private void showPublishUI(RSConnectPublishInput input) 376 { 377 final RSConnectActionEvent event = input.getOriginatingEvent(); 378 if (input.getContentType() == CONTENT_TYPE_PLOT || 379 input.getContentType() == CONTENT_TYPE_HTML || 380 input.getContentType() == CONTENT_TYPE_PRES) 381 { 382 if (!input.isConnectUIEnabled() && input.isExternalUIEnabled()) 383 { 384 publishAsRPubs(event); 385 } 386 else if (input.isConnectUIEnabled() && input.isExternalUIEnabled()) 387 { 388 publishWithWizard(input); 389 } 390 else if (input.isConnectUIEnabled() && !input.isExternalUIEnabled()) 391 { 392 publishAsStatic(input); 393 } 394 } 395 else if (input.isWebsiteContentType() || 396 (input.getContentType() == CONTENT_TYPE_DOCUMENT && input.isWebsiteRmd())) 397 { 398 if (input.hasDocOutput() || input.isWebsiteContentType()) 399 { 400 publishWithWizard(input); 401 } 402 else 403 { 404 publishAsCode(event, input.getWebsiteDir(), false); 405 } 406 } 407 else if (input.getContentType() == CONTENT_TYPE_DOCUMENT) 408 { 409 if (input.isShiny()) 410 { 411 if (input.isMultiRmd()) 412 { 413 // multiple Shiny doc 414 publishWithWizard(input); 415 } 416 else 417 { 418 // single Shiny doc 419 publishAsCode(event, input.getWebsiteDir(), true); 420 } 421 } 422 else 423 { 424 if (input.isConnectUIEnabled()) 425 { 426 // need to disambiguate between code/output and/or 427 // single/multi page 428 publishWithWizard(input); 429 } 430 else if (!input.isSelfContained()) 431 { 432 // we should generally hide the button in this case 433 display_.showErrorMessage("Content Not Publishable", 434 "Only self-contained documents can currently be " + 435 "published to RPubs."); 436 } 437 else 438 { 439 // RStudio Connect is disabled, go straight to RPubs 440 publishAsRPubs(event); 441 } 442 } 443 } 444 else if (input.getContentType() == CONTENT_TYPE_APP || 445 input.getContentType() == CONTENT_TYPE_APP_SINGLE) 446 { 447 publishAsCode(event, null, true); 448 } 449 else if (input.getContentType() == CONTENT_TYPE_PLUMBER_API) 450 { 451 if (!input.isConnectUIEnabled()) 452 { 453 display_.showErrorMessage("API Not Publishable", 454 "Publishing to RStudio Connect is disabled in the Publishing options."); 455 } 456 else 457 { 458 publishAsCode(event, null, false); 459 } 460 } 461 } 462 publishAsCode(RSConnectActionEvent event, String websiteDir, boolean isShiny)463 private void publishAsCode(RSConnectActionEvent event, String websiteDir, boolean isShiny) 464 { 465 boolean isAPI = event.getContentType() == CONTENT_TYPE_PLUMBER_API; 466 467 RSConnectPublishSource source = null; 468 if (event.getContentType() == CONTENT_TYPE_APP || 469 event.getContentType() == CONTENT_TYPE_APP_SINGLE || 470 isAPI) 471 { 472 if (StringUtil.getExtension(event.getPath()).equalsIgnoreCase("r")) 473 { 474 FileSystemItem rFile = FileSystemItem.createFile(event.getPath()); 475 476 // use the directory for the deployment record when publishing APIs or 477 // directory-based apps; use the file itself when publishing 478 // single-file apps 479 source = new RSConnectPublishSource(rFile.getParentPathString(), 480 event.getContentType() == CONTENT_TYPE_APP_SINGLE ? 481 rFile.getName() : 482 rFile.getParentPathString(), 483 isAPI); 484 485 } 486 else 487 { 488 source = new RSConnectPublishSource(event.getPath(), 489 event.getPath(), 490 isAPI); 491 } 492 } 493 else 494 { 495 source = new RSConnectPublishSource(event.getPath(), websiteDir, 496 false, false, isShiny, 497 event.getContentType() == RSConnect.CONTENT_TYPE_QUARTO_WEBSITE, null, event.getContentType()); 498 } 499 500 // detect quarto 501 if (event.getFromPreview() != null) 502 { 503 source.setIsQuarto(event.getFromPreview().isQuarto()); 504 } 505 506 publishAsFiles(event, source); 507 } 508 publishAsStatic(RSConnectPublishInput input)509 private void publishAsStatic(RSConnectPublishInput input) 510 { 511 RSConnectPublishSource source = null; 512 if (input.getContentType() == RSConnect.CONTENT_TYPE_DOCUMENT || 513 input.isWebsiteContentType()) 514 { 515 source = new RSConnectPublishSource( 516 input.getOriginatingEvent().getFromPreview(), 517 input.getWebsiteDir(), 518 input.isSelfContained(), 519 true, 520 input.isShiny(), 521 input.getDescription()); 522 } 523 else 524 { 525 source = new RSConnectPublishSource( 526 input.getOriginatingEvent().getHtmlFile(), 527 input.getWebsiteDir(), 528 input.isSelfContained(), 529 true, 530 input.isShiny(), 531 input.isQuarto(), 532 input.getDescription(), 533 input.getContentType()); 534 } 535 publishAsFiles(input.getOriginatingEvent(), source); 536 } 537 publishAsFiles(RSConnectActionEvent event, RSConnectPublishSource source)538 private void publishAsFiles(RSConnectActionEvent event, 539 RSConnectPublishSource source) 540 { 541 RSConnectDeployDialog dialog = 542 new RSConnectDeployDialog( 543 event.getContentType(), 544 server_, this, display_, 545 source, 546 event.getFromPrevious()); 547 dialog.showModal(); 548 } 549 publishWithWizard(final RSConnectPublishInput input)550 private void publishWithWizard(final RSConnectPublishInput input) 551 { 552 RSConnectPublishWizard wizard = 553 new RSConnectPublishWizard(input, 554 new ProgressOperationWithInput<RSConnectPublishResult>() 555 { 556 @Override 557 public void execute(RSConnectPublishResult result, 558 ProgressIndicator indicator) 559 { 560 switch (result.getPublishType()) 561 { 562 case RSConnectPublishResult.PUBLISH_STATIC: 563 case RSConnectPublishResult.PUBLISH_CODE: 564 // always launch the browser--the wizard implies we're 565 // doing a first-time publish, and we may need to do some 566 // post-publish configuration 567 fireRSConnectPublishEvent(result, true); 568 indicator.onCompleted(); 569 break; 570 case RSConnectPublishResult.PUBLISH_RPUBS: 571 uploadToRPubs(input, result, indicator); 572 break; 573 } 574 } 575 }); 576 wizard.showModal(); 577 } 578 579 @Override onRSConnectDeployInitiated( final RSConnectDeployInitiatedEvent event)580 public void onRSConnectDeployInitiated( 581 final RSConnectDeployInitiatedEvent event) 582 { 583 // shortcut: when deploying static content we don't need to do any linting 584 if (event.getSettings().getAsStatic()) 585 { 586 doDeployment(event); 587 return; 588 } 589 590 // get lint results for the file or directory being deployed, as 591 // appropriate 592 server_.getLintResults(event.getSource().getDeployKey(), 593 new ServerRequestCallback<RSConnectLintResults>() 594 { 595 @Override 596 public void onResponseReceived(RSConnectLintResults results) 597 { 598 if (results.getErrorMessage().length() > 0) 599 { 600 display_.showYesNoMessage(GlobalDisplay.MSG_QUESTION, 601 "Lint Failed", 602 "The content you tried to publish could not be checked " + 603 "for errors. Do you want to proceed? \n\n" + 604 results.getErrorMessage(), false, 605 new ProgressOperation() 606 { 607 @Override 608 public void execute(ProgressIndicator indicator) 609 { 610 // "Publish Anyway" 611 doDeployment(event); 612 indicator.onCompleted(); 613 } 614 }, 615 new ProgressOperation() 616 { 617 @Override 618 public void execute(ProgressIndicator indicator) 619 { 620 // "Cancel" 621 indicator.onCompleted(); 622 } 623 }, 624 "Publish Anyway", "Cancel", false); 625 } 626 else if (results.hasLint()) 627 { 628 display_.showYesNoMessage(GlobalDisplay.MSG_QUESTION, 629 "Publish Content Issues Found", 630 "Some issues were found in your content, which may " + 631 "prevent it from working correctly after publishing. " + 632 "Do you want to review these issues or publish anyway? " 633 , false, 634 new ProgressOperation() 635 { 636 @Override 637 public void execute(ProgressIndicator indicator) 638 { 639 // "Review Issues" -- we automatically show the 640 // markers so they're already behind the dialog. 641 indicator.onCompleted(); 642 } 643 }, 644 new ProgressOperation() { 645 @Override 646 public void execute(ProgressIndicator indicator) 647 { 648 // "Publish Anyway" 649 doDeployment(event); 650 indicator.onCompleted(); 651 } 652 }, 653 "Review Issues", "Publish Anyway", true); 654 } 655 else 656 { 657 // no lint and no errors -- good to go for deployment 658 doDeployment(event); 659 } 660 } 661 662 @Override 663 public void onError(ServerError error) 664 { 665 // we failed to lint, which is not encouraging, but we don't want to 666 // fail the whole deployment lest a balky linter prevent people from 667 // getting their work published, so forge on ahead. 668 doDeployment(event); 669 } 670 }); 671 } 672 673 @Override onRSConnectDeploymentCompleted( RSConnectDeploymentCompletedEvent event)674 public void onRSConnectDeploymentCompleted( 675 RSConnectDeploymentCompletedEvent event) 676 { 677 if (launchBrowser_ && event.succeeded()) 678 { 679 display_.openWindow(event.getUrl()); 680 } 681 } 682 683 @Override onRSConnectDeploymentCancelled( RSConnectDeploymentCancelledEvent event)684 public void onRSConnectDeploymentCancelled( 685 RSConnectDeploymentCancelledEvent event) 686 { 687 display_.showYesNoMessage(GlobalDisplay.MSG_QUESTION, 688 "Stop deployment?", 689 "Do you want to stop the deployment process? If the server has already " + 690 "received the content, it will still be published.", 691 false, // include cancel 692 () -> { 693 server_.cancelPublish(new ServerRequestCallback<Boolean>() 694 { 695 @Override 696 public void onError(ServerError error) 697 { 698 display_.showErrorMessage("Error Stopping Deployment", 699 error.getMessage()); 700 } 701 702 @Override 703 public void onResponseReceived(Boolean result) 704 { 705 if (!result) 706 { 707 display_.showErrorMessage("Could not cancel deployment", 708 "The deployment could not be cancelled; it is not running, or termination failed."); 709 } 710 } 711 }); 712 }, 713 null, 714 null, 715 "Stop deployment", 716 "Cancel", 717 false); 718 } 719 720 @Override onRSConnectDeploymentFailed( final RSConnectDeploymentFailedEvent event)721 public void onRSConnectDeploymentFailed( 722 final RSConnectDeploymentFailedEvent event) 723 { 724 String failedPath = event.getData().getPath(); 725 // if this looks like an API call, process the path to get the 'bare' 726 // server URL 727 int pos = failedPath.indexOf("__api__"); 728 if (pos < 1) 729 { 730 // if not, just get the host 731 pos = failedPath.indexOf("/", 10) + 1; 732 } 733 if (pos > 0) 734 { 735 failedPath = failedPath.substring(0, pos); 736 } 737 final String serverUrl = failedPath; 738 739 new ModalDialogBase(Roles.getAlertdialogRole()) 740 { 741 @Override 742 protected Widget createMainWidget() 743 { 744 setText("Publish Failed"); 745 addOkButton(new ThemedButton("OK", new ClickHandler() 746 { 747 @Override 748 public void onClick(ClickEvent arg0) 749 { 750 closeDialog(); 751 } 752 })); 753 HorizontalPanel panel = new HorizontalPanel(); 754 Image errorImage = 755 new Image(new ImageResource2x(MessageDialogImages.INSTANCE.dialog_error2x())); 756 errorImage.getElement().getStyle().setMarginTop(1, Unit.EM); 757 errorImage.getElement().getStyle().setMarginRight(1, Unit.EM); 758 panel.add(errorImage); 759 panel.add(new HTML("<p>Your content could not be published because " + 760 "of a problem on the server.</p>" + 761 "<p>More information may be available on the server's home " + 762 "page:</p>" + 763 "<p><a href=\"" + serverUrl + "\">" + serverUrl + "</a>" + 764 "</p>" + 765 "<p>If the error persists, contact the server's " + 766 "administrator.</p>" + 767 "<p><small>Error code: " + event.getData().getHttpStatus() + 768 "</small></p>")); 769 return panel; 770 } 771 }.showModal(); 772 } 773 ensureSessionInit()774 public void ensureSessionInit() 775 { 776 if (sessionInited_) 777 return; 778 779 // "Manage accounts" can be invoked any time we're permitted to 780 // publish 781 commands_.rsconnectManageAccounts().setVisible( 782 SessionUtils.showPublishUi(session_, pUserState_.get())); 783 784 // This object keeps track of the most recent deployment we made of each 785 // directory, and is used to default directory deployments to last-used 786 // settings. 787 new JSObjectStateValue( 788 "rsconnect", 789 "rsconnectDirectories", 790 ClientState.PERSISTENT, 791 session_.getSessionInfo().getClientState(), 792 false) 793 { 794 @Override 795 protected void onInit(JsObject value) 796 { 797 dirState_ = (RSConnectDirectoryState) (value == null ? 798 RSConnectDirectoryState.create() : 799 value.cast()); 800 } 801 802 @Override 803 protected JsObject getValue() 804 { 805 dirStateDirty_ = false; 806 return (JsObject) (dirState_ == null ? 807 RSConnectDirectoryState.create().cast() : 808 dirState_.cast()); 809 } 810 811 @Override 812 protected boolean hasChanged() 813 { 814 return dirStateDirty_; 815 } 816 }; 817 818 sessionInited_ = true; 819 } 820 deployFromSatellite( String sourceFile, String deployDir, String deployFile, String websiteDir, String description, JsArrayString deployFiles, JsArrayString additionalFiles, JsArrayString ignoredFiles, boolean isSelfContained, boolean isShiny, boolean asMultiple, boolean asStatic, boolean isQuarto, boolean launch, JavaScriptObject record)821 public static native void deployFromSatellite( 822 String sourceFile, 823 String deployDir, 824 String deployFile, 825 String websiteDir, 826 String description, 827 JsArrayString deployFiles, 828 JsArrayString additionalFiles, 829 JsArrayString ignoredFiles, 830 boolean isSelfContained, 831 boolean isShiny, 832 boolean asMultiple, 833 boolean asStatic, 834 boolean isQuarto, 835 boolean launch, 836 JavaScriptObject record) /*-{ 837 $wnd.opener.deployToRSConnect(sourceFile, deployDir, deployFile, 838 websiteDir, description, deployFiles, 839 additionalFiles, ignoredFiles, isSelfContained, 840 isShiny, asMultiple, asStatic, isQuarto, launch, 841 record); 842 }-*/; 843 844 showRSConnectUI()845 public static boolean showRSConnectUI() 846 { 847 return true; 848 } 849 contentTypeDesc(int contentType)850 public static String contentTypeDesc(int contentType) 851 { 852 switch(contentType) 853 { 854 case RSConnect.CONTENT_TYPE_APP: 855 case RSConnect.CONTENT_TYPE_APP_SINGLE: 856 return "Application"; 857 case RSConnect.CONTENT_TYPE_PLOT: 858 return "Plot"; 859 case RSConnect.CONTENT_TYPE_HTML: 860 return "HTML"; 861 case RSConnect.CONTENT_TYPE_DOCUMENT: 862 return "Document"; 863 case RSConnect.CONTENT_TYPE_PRES: 864 return "Presentation"; 865 case RSConnect.CONTENT_TYPE_WEBSITE: 866 return "Website"; 867 case RSConnect.CONTENT_TYPE_PLUMBER_API: 868 return "API"; 869 case RSConnect.CONTENT_TYPE_QUARTO_WEBSITE: 870 return "Quarto Website"; 871 } 872 return "Content"; 873 } 874 fireRSConnectPublishEvent(RSConnectPublishResult result, boolean launchBrowser)875 public void fireRSConnectPublishEvent(RSConnectPublishResult result, 876 boolean launchBrowser) 877 { 878 if (Satellite.isCurrentWindowSatellite()) 879 { 880 // in a satellite window, call back to the main window to do a 881 // deployment 882 RSConnect.deployFromSatellite( 883 result.getSource().getSourceFile(), 884 result.getSource().getDeployDir(), 885 result.getSource().getDeployFile(), 886 result.getSource().getWebsiteDir(), 887 result.getSource().getDescription(), 888 JsArrayUtil.toJsArrayString( 889 result.getSettings().getDeployFiles()), 890 JsArrayUtil.toJsArrayString( 891 result.getSettings().getAdditionalFiles()), 892 JsArrayUtil.toJsArrayString( 893 result.getSettings().getIgnoredFiles()), 894 result.getSource().isSelfContained(), 895 result.getSource().isShiny(), 896 result.getSettings().getAsMultiple(), 897 result.getSettings().getAsStatic(), 898 result.getSource().isQuarto(), 899 launchBrowser, 900 RSConnectDeploymentRecord.create(result.getAppName(), 901 result.getAppTitle(), result.getAppId(), result.getAccount(), "")); 902 903 // we can't raise the main window if we aren't in desktop mode, so show 904 // a dialog to guide the user there 905 if (!Desktop.hasDesktopFrame()) 906 { 907 display_.showMessage(GlobalDisplay.MSG_INFO, "Deployment Started", 908 "RStudio is deploying " + result.getAppName() + ". " + 909 "Check the Deploy console tab in the main window for " + 910 "status updates. "); 911 } 912 } 913 else 914 { 915 // in the main window, initiate the deployment directly 916 events_.fireEvent(new RSConnectDeployInitiatedEvent( 917 result.getSource(), 918 result.getSettings(), 919 launchBrowser, 920 RSConnectDeploymentRecord.create(result.getAppName(), 921 result.getAppTitle(), result.getAppId(), result.getAccount(), ""))); 922 } 923 } 924 925 // Private methods --------------------------------------------------------- showUnsupportedRPubsFormatMessage()926 private void showUnsupportedRPubsFormatMessage() 927 { 928 display_.showErrorMessage("Unsupported Document Format", 929 "Only documents rendered to HTML can be published to RPubs. " + 930 "To publish this document, click Knit or Preview to render it to HTML, then " + 931 "click the Publish button above the rendered document."); 932 } 933 uploadToRPubs(RSConnectPublishInput input, RSConnectPublishResult result, final ProgressIndicator indicator)934 private void uploadToRPubs(RSConnectPublishInput input, 935 RSConnectPublishResult result, 936 final ProgressIndicator indicator) 937 { 938 if (input.getContentType() == CONTENT_TYPE_DOCUMENT) 939 { 940 if (!input.hasDocOutput()) 941 { 942 display_.showErrorMessage("Publish Document", 943 "Only rendered documents can be published to RPubs. " + 944 "To publish this document, click Knit or Preview to render it to HTML, then " + 945 "click the Publish button above the rendered document."); 946 indicator.onCompleted(); 947 return; 948 } 949 else if (!supportedRPubsDocExtension(input.getDocOutput())) 950 { 951 showUnsupportedRPubsFormatMessage(); 952 indicator.onCompleted(); 953 return; 954 } 955 } 956 957 RPubsUploader uploader = new RPubsUploader(rpubsServer_, display_, 958 events_, "rpubs-" + rpubsCount_++); 959 String contentType = contentTypeDesc(input.getContentType()); 960 indicator.onProgress("Uploading " + contentType); 961 uploader.setOnUploadComplete(new CommandWithArg<Boolean>() 962 { 963 @Override 964 public void execute(Boolean arg) 965 { 966 indicator.onCompleted(); 967 } 968 }); 969 uploader.performUpload(contentType, 970 input.getSourceRmd() == null ? null : 971 input.getSourceRmd().getPath(), 972 input.getOriginatingEvent().getHtmlFile(), 973 input.getOriginatingEvent().getFromPrevious() == null ? "" : 974 input.getOriginatingEvent().getFromPrevious().getBundleId(), 975 false); 976 } 977 handleRSConnectAction(RSConnectActionEvent event)978 private void handleRSConnectAction(RSConnectActionEvent event) 979 { 980 if (event.getAction() == RSConnectActionEvent.ACTION_TYPE_DEPLOY) 981 { 982 // ignore this request if there's already a modal up 983 if (ModalDialogTracker.numModalsShowing() > 0) 984 return; 985 986 // show publish UI appropriate to the type of content being deployed 987 showPublishUI(event); 988 } 989 else if (event.getAction() == RSConnectActionEvent.ACTION_TYPE_CONFIGURE) 990 { 991 configureShinyApp(FilePathUtils.dirFromFile(event.getPath())); 992 } 993 } 994 doDeployment(final RSConnectDeployInitiatedEvent event)995 private void doDeployment(final RSConnectDeployInitiatedEvent event) 996 { 997 server_.publishContent(event.getSource(), 998 event.getRecord().getAccountName(), 999 event.getRecord().getServer(), 1000 event.getRecord().getName(), 1001 event.getRecord().getTitle(), 1002 event.getRecord().getAppId(), 1003 event.getSettings(), 1004 new ServerRequestCallback<Boolean>() 1005 { 1006 @Override 1007 public void onResponseReceived(Boolean status) 1008 { 1009 if (status) 1010 { 1011 dirState_.addDeployment(event.getSource().getDeployDir(), 1012 event.getRecord()); 1013 dirStateDirty_ = true; 1014 if (event.getSource().getContentCategory() == 1015 RSConnect.CONTENT_CATEGORY_PLOT) 1016 { 1017 plotMru_.addPlotMruEntry(event.getRecord().getAccountName(), 1018 event.getRecord().getServer(), 1019 event.getRecord().getName(), 1020 event.getRecord().getTitle()); 1021 } 1022 launchBrowser_ = event.getLaunchBrowser(); 1023 events_.fireEvent(new RSConnectDeploymentStartedEvent( 1024 event.getSource().isWebsiteRmd() ? "" : 1025 event.getSource().getDeployKey(), 1026 event.getSource().getDescription())); 1027 } 1028 else 1029 { 1030 display_.showErrorMessage("Deployment In Progress", 1031 "Another deployment is currently in progress; only one " + 1032 "deployment can be performed at a time."); 1033 } 1034 } 1035 1036 @Override 1037 public void onError(ServerError error) 1038 { 1039 display_.showErrorMessage("Error Deploying Application", 1040 "Could not deploy application '" + 1041 event.getRecord().getName() + 1042 "': " + error.getMessage()); 1043 } 1044 }); 1045 } 1046 1047 // Manage, step 1: create a list of apps deployed from this directory configureShinyApp(final String dir)1048 private void configureShinyApp(final String dir) 1049 { 1050 server_.getRSConnectDeployments(dir, 1051 "", 1052 new ServerRequestCallback<JsArray<RSConnectDeploymentRecord>>() 1053 { 1054 @Override 1055 public void onResponseReceived( 1056 JsArray<RSConnectDeploymentRecord> records) 1057 { 1058 configureShinyApp(dir, records); 1059 } 1060 @Override 1061 public void onError(ServerError error) 1062 { 1063 display_.showErrorMessage("Error Configuring Application", 1064 "Could not determine application deployments for '" + 1065 dir + "':" + error.getMessage()); 1066 } 1067 }); 1068 } 1069 1070 // Manage, step 2: Get the status of the applications from the server configureShinyApp(final String dir, JsArray<RSConnectDeploymentRecord> records)1071 private void configureShinyApp(final String dir, 1072 JsArray<RSConnectDeploymentRecord> records) 1073 { 1074 if (records.length() == 0) 1075 { 1076 display_.showMessage(GlobalDisplay.MSG_INFO, "No Deployments Found", 1077 "No application deployments were found for '" + dir + "'"); 1078 return; 1079 } 1080 1081 // If we know the most recent deployment of the directory, act on that 1082 // deployment by default 1083 final ArrayList<RSConnectDeploymentRecord> recordList = new ArrayList<>(); 1084 RSConnectDeploymentRecord lastRecord = dirState_.getLastDeployment(dir); 1085 if (lastRecord != null) 1086 { 1087 recordList.add(lastRecord); 1088 } 1089 for (int i = 0; i < records.length(); i++) 1090 { 1091 RSConnectDeploymentRecord record = records.get(i); 1092 if (lastRecord == null) 1093 { 1094 recordList.add(record); 1095 } 1096 else 1097 { 1098 if (record.getUrl() == lastRecord.getUrl()) 1099 recordList.set(0, record); 1100 } 1101 } 1102 1103 // We need to further filter the list by deployments that are 1104 // eligible for termination (i.e. are currently running) 1105 server_.getRSConnectAppList(recordList.get(0).getAccountName(), 1106 recordList.get(0).getServer(), 1107 new ServerRequestCallback<JsArray<RSConnectApplicationInfo>>() 1108 { 1109 @Override 1110 public void onResponseReceived(JsArray<RSConnectApplicationInfo> apps) 1111 { 1112 configureShinyApp(dir, apps, recordList); 1113 } 1114 @Override 1115 public void onError(ServerError error) 1116 { 1117 display_.showErrorMessage("Error Listing Applications", 1118 error.getMessage()); 1119 } 1120 }); 1121 } 1122 1123 // Manage, step 3: compare the deployments and apps active on the server 1124 // until we find a running app from the current directory configureShinyApp(String dir, JsArray<RSConnectApplicationInfo> apps, List<RSConnectDeploymentRecord> records)1125 private void configureShinyApp(String dir, 1126 JsArray<RSConnectApplicationInfo> apps, 1127 List<RSConnectDeploymentRecord> records) 1128 { 1129 for (int i = 0; i < records.size(); i++) 1130 { 1131 for (int j = 0; j < apps.length(); j++) 1132 { 1133 RSConnectApplicationInfo candidate = apps.get(j); 1134 if (candidate.getName() == records.get(i).getName()) 1135 { 1136 // show the management ui 1137 display_.openWindow(candidate.getConfigUrl()); 1138 return; 1139 } 1140 } 1141 } 1142 display_.showMessage(GlobalDisplay.MSG_INFO, 1143 "No Running Deployments Found", "No applications deployed from '" + 1144 dir + "' appear to be running."); 1145 } 1146 exportNativeCallbacks()1147 private final native void exportNativeCallbacks() /*-{ 1148 var thiz = this; 1149 $wnd.deployToRSConnect = $entry( 1150 function(sourceFile, deployDir, deployFile, websiteDir, description, deployFiles, additionalFiles, ignoredFiles, isSelfContained, isShiny, asMultiple, asStatic, isQuarto, launch, record) { 1151 thiz.@org.rstudio.studio.client.rsconnect.RSConnect::deployToRSConnect(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/google/gwt/core/client/JsArrayString;Lcom/google/gwt/core/client/JsArrayString;Lcom/google/gwt/core/client/JsArrayString;ZZZZZZLcom/google/gwt/core/client/JavaScriptObject;)(sourceFile, deployDir, deployFile, websiteDir, description, deployFiles, additionalFiles, ignoredFiles, isSelfContained, isShiny, asMultiple, asStatic, isQuarto, launch, record); 1152 } 1153 ); 1154 }-*/; 1155 deployToRSConnect(String sourceFile, String deployDir, String deployFile, String websiteDir, String description, JsArrayString deployFiles, JsArrayString additionalFiles, JsArrayString ignoredFiles, boolean isSelfContained, boolean isShiny, boolean asMultiple, boolean asStatic, boolean isQuarto, boolean launch, JavaScriptObject jsoRecord)1156 private void deployToRSConnect(String sourceFile, 1157 String deployDir, 1158 String deployFile, 1159 String websiteDir, 1160 String description, 1161 JsArrayString deployFiles, 1162 JsArrayString additionalFiles, 1163 JsArrayString ignoredFiles, 1164 boolean isSelfContained, 1165 boolean isShiny, 1166 boolean asMultiple, 1167 boolean asStatic, 1168 boolean isQuarto, 1169 boolean launch, 1170 JavaScriptObject jsoRecord) 1171 { 1172 // this can be invoked by a satellite, so bring the main frame to the 1173 // front if we can 1174 if (Desktop.hasDesktopFrame()) 1175 Desktop.getFrame().bringMainFrameToFront(); 1176 else 1177 WindowEx.get().focus(); 1178 1179 ArrayList<String> deployFilesList = 1180 JsArrayUtil.fromJsArrayString(deployFiles); 1181 ArrayList<String> additionalFilesList = 1182 JsArrayUtil.fromJsArrayString(additionalFiles); 1183 ArrayList<String> ignoredFilesList = 1184 JsArrayUtil.fromJsArrayString(ignoredFiles); 1185 1186 RSConnectDeploymentRecord record = jsoRecord.cast(); 1187 events_.fireEvent(new RSConnectDeployInitiatedEvent( 1188 new RSConnectPublishSource(sourceFile, deployDir, deployFile, 1189 websiteDir, isSelfContained, asStatic, isShiny, isQuarto, description), 1190 new RSConnectPublishSettings(deployFilesList, 1191 additionalFilesList, ignoredFilesList, asMultiple, asStatic), 1192 launch, record)); 1193 } 1194 fillInputFromDoc(final RSConnectPublishInput input, final String docPath, final CommandWithArg<RSConnectPublishInput> onComplete)1195 private void fillInputFromDoc(final RSConnectPublishInput input, 1196 final String docPath, 1197 final CommandWithArg<RSConnectPublishInput> onComplete) 1198 { 1199 boolean isQuarto = false; 1200 if (input.getOriginatingEvent() != null && 1201 input.getOriginatingEvent().getFromPreview() != null) 1202 { 1203 isQuarto = input.getOriginatingEvent().getFromPreview().isQuarto(); 1204 } 1205 1206 if (isQuarto) 1207 { 1208 // Quarto metadata lookup can take a couple of seconds; ensure the 1209 // user can see some progress while we're doing it 1210 final ProgressIndicator indicator = display_.getProgressIndicator("Error"); 1211 indicator.onProgress("Preparing for Publish..."); 1212 1213 server_.quartoPublishDetails( 1214 docPath, 1215 new ServerRequestCallback<QmdPublishDetails>() 1216 { 1217 @Override 1218 public void onResponseReceived(QmdPublishDetails details) 1219 { 1220 indicator.onCompleted(); 1221 RenderedDocPreview previewParams = input.getOriginatingEvent().getFromPreview(); 1222 if (previewParams != null) 1223 { 1224 if (StringUtil.isNullOrEmpty(details.website_output_dir)) 1225 previewParams.setOutputFile(details.output_file); 1226 else 1227 previewParams.setOutputFile(details.website_output_dir); 1228 previewParams.setWebsiteDir(details.website_dir); 1229 } 1230 input.setIsMultiRmd(false); 1231 input.setIsQuarto(true); 1232 input.setIsShiny(details.is_shiny_qmd); 1233 input.setIsSelfContained(details.is_self_contained); 1234 input.setHasConnectAccount(details.has_connect_account); 1235 input.setWebsiteDir(details.website_dir); 1236 input.setWebsiteOutputDir(details.website_output_dir); 1237 1238 onComplete.execute(input); 1239 } 1240 1241 @Override 1242 public void onError(ServerError error) 1243 { 1244 indicator.onError(error.getMessage()); 1245 onComplete.execute(null); 1246 } 1247 } 1248 ); 1249 } 1250 else 1251 { 1252 server_.getRmdPublishDetails( 1253 docPath, 1254 new ServerRequestCallback<RmdPublishDetails>() 1255 { 1256 @Override 1257 public void onResponseReceived(RmdPublishDetails details) 1258 { 1259 input.setIsMultiRmd(details.is_multi_rmd); 1260 input.setIsShiny(details.is_shiny_rmd); 1261 input.setIsSelfContained(details.is_self_contained); 1262 input.setIsQuarto(false); 1263 input.setHasConnectAccount(details.has_connect_account); 1264 input.setWebsiteDir(details.website_dir); 1265 input.setWebsiteOutputDir(details.website_output_dir); 1266 if (StringUtil.isNullOrEmpty(input.getDescription())) 1267 { 1268 if (!StringUtil.isNullOrEmpty(details.title)) 1269 { 1270 // set the description from the document title, if we 1271 // have it 1272 input.setDescription(details.title); 1273 } 1274 else 1275 { 1276 // set the description from the document name 1277 input.setDescription( 1278 FilePathUtils.fileNameSansExtension(docPath)); 1279 } 1280 } 1281 onComplete.execute(input); 1282 } 1283 1284 @Override 1285 public void onError(ServerError error) 1286 { 1287 // this is unlikely since the RPC does little work, but 1288 // we can't offer the right choices in the wizard if we 1289 // don't know what we're working with. 1290 display_.showErrorMessage("Could Not Publish", 1291 error.getMessage()); 1292 onComplete.execute(null); 1293 } 1294 }); 1295 } 1296 } 1297 1298 private final Commands commands_; 1299 private final GlobalDisplay display_; 1300 private final Session session_; 1301 private final RSConnectServerOperations server_; 1302 private final RPubsServerOperations rpubsServer_; 1303 private final SourceServerOperations sourceServer_; 1304 private final DependencyManager dependencyManager_; 1305 private final EventBus events_; 1306 private final RSAccountConnector connector_; 1307 private final Provider<UserPrefs> pUserPrefs_; 1308 private final Provider<UserState> pUserState_; 1309 private final PlotPublishMRUList plotMru_; 1310 1311 private boolean launchBrowser_ = false; 1312 private boolean sessionInited_ = false; 1313 private boolean depsPending_ = false; 1314 private String lastDeployedServer_ = ""; 1315 1316 // incremented on each RPubs publish (to provide a unique context) 1317 private static int rpubsCount_ = 0; 1318 1319 private RSConnectDirectoryState dirState_; 1320 private boolean dirStateDirty_ = false; 1321 1322 public final static String CLOUD_SERVICE_NAME = "ShinyApps.io"; 1323 1324 // No/unknown content type 1325 public final static int CONTENT_TYPE_NONE = 0; 1326 1327 // A single HTML file representing a plot 1328 public final static int CONTENT_TYPE_PLOT = 1; 1329 1330 // A document (.Rmd, .md, etc.), 1331 public final static int CONTENT_TYPE_DOCUMENT = 2; 1332 1333 // A Shiny application 1334 public final static int CONTENT_TYPE_APP = 3; 1335 1336 // A single-file Shiny application 1337 public final static int CONTENT_TYPE_APP_SINGLE = 4; 1338 1339 // Standalone HTML (from HTML widgets/viewer pane, etc.) 1340 public final static int CONTENT_TYPE_HTML = 5; 1341 1342 // A .Rpres presentation 1343 public final static int CONTENT_TYPE_PRES = 6; 1344 1345 // A page in an R Markdown website 1346 public final static int CONTENT_TYPE_WEBSITE = 7; 1347 1348 // Plumber API 1349 public final static int CONTENT_TYPE_PLUMBER_API = 8; 1350 1351 // A Quarto website 1352 public final static int CONTENT_TYPE_QUARTO_WEBSITE = 9; 1353 1354 public final static String CONTENT_CATEGORY_PLOT = "plot"; 1355 public final static String CONTENT_CATEGORY_SITE = "site"; 1356 public final static String CONTENT_CATEGORY_API = "api"; 1357 } 1358