/*
* RSConnect.java
*
* Copyright (C) 2009-15 by RStudio, Inc.
*
* Unless you have received this program directly from RStudio pursuant
* to the terms of a commercial license agreement with RStudio, then
* this program is licensed to you under the terms of version 3 of the
* GNU Affero General Public License. This program is distributed WITHOUT
* ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
* MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
* AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
*
*/
package org.rstudio.studio.client.rsconnect;
import java.util.ArrayList;
import java.util.List;
import org.rstudio.core.client.CommandWithArg;
import org.rstudio.core.client.JsArrayUtil;
import org.rstudio.core.client.StringUtil;
import org.rstudio.core.client.command.CommandBinder;
import org.rstudio.core.client.dom.WindowEx;
import org.rstudio.core.client.files.FileSystemItem;
import org.rstudio.core.client.js.JsObject;
import org.rstudio.core.client.resources.ImageResource2x;
import org.rstudio.core.client.widget.ModalDialogBase;
import org.rstudio.core.client.widget.ModalDialogTracker;
import org.rstudio.core.client.widget.ProgressIndicator;
import org.rstudio.core.client.widget.ProgressOperation;
import org.rstudio.core.client.widget.ProgressOperationWithInput;
import org.rstudio.core.client.widget.ThemedButton;
import org.rstudio.core.client.widget.images.MessageDialogImages;
import org.rstudio.studio.client.application.Desktop;
import org.rstudio.studio.client.application.events.EventBus;
import org.rstudio.studio.client.common.FilePathUtils;
import org.rstudio.studio.client.common.GlobalDisplay;
import org.rstudio.studio.client.common.dependencies.DependencyManager;
import org.rstudio.studio.client.common.rpubs.RPubsUploader;
import org.rstudio.studio.client.common.rpubs.model.RPubsServerOperations;
import org.rstudio.studio.client.common.rpubs.ui.RPubsUploadDialog;
import org.rstudio.studio.client.common.satellite.Satellite;
import org.rstudio.studio.client.rsconnect.events.RSConnectActionEvent;
import org.rstudio.studio.client.rsconnect.events.RSConnectDeployInitiatedEvent;
import org.rstudio.studio.client.rsconnect.events.RSConnectDeploymentCompletedEvent;
import org.rstudio.studio.client.rsconnect.events.RSConnectDeploymentFailedEvent;
import org.rstudio.studio.client.rsconnect.events.RSConnectDeploymentStartedEvent;
import org.rstudio.studio.client.rsconnect.model.PlotPublishMRUList;
import org.rstudio.studio.client.rsconnect.model.RSConnectApplicationInfo;
import org.rstudio.studio.client.rsconnect.model.RSConnectDeploymentRecord;
import org.rstudio.studio.client.rsconnect.model.RSConnectDirectoryState;
import org.rstudio.studio.client.rsconnect.model.RSConnectLintResults;
import org.rstudio.studio.client.rsconnect.model.RSConnectPublishInput;
import org.rstudio.studio.client.rsconnect.model.RSConnectPublishResult;
import org.rstudio.studio.client.rsconnect.model.RSConnectPublishSettings;
import org.rstudio.studio.client.rsconnect.model.RSConnectPublishSource;
import org.rstudio.studio.client.rsconnect.model.RSConnectServerOperations;
import org.rstudio.studio.client.rsconnect.model.RmdPublishDetails;
import org.rstudio.studio.client.rsconnect.ui.RSAccountConnector;
import org.rstudio.studio.client.rsconnect.ui.RSConnectDeployDialog;
import org.rstudio.studio.client.rsconnect.ui.RSConnectPublishWizard;
import org.rstudio.studio.client.server.ServerError;
import org.rstudio.studio.client.server.ServerRequestCallback;
import org.rstudio.studio.client.workbench.commands.Commands;
import org.rstudio.studio.client.workbench.events.SessionInitEvent;
import org.rstudio.studio.client.workbench.events.SessionInitHandler;
import org.rstudio.studio.client.workbench.model.ClientState;
import org.rstudio.studio.client.workbench.model.Session;
import org.rstudio.studio.client.workbench.model.SessionUtils;
import org.rstudio.studio.client.workbench.model.helper.JSObjectStateValue;
import org.rstudio.studio.client.workbench.prefs.model.UIPrefs;
import org.rstudio.studio.client.workbench.views.source.model.SourceServerOperations;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.core.client.JsArrayString;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.Widget;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
@Singleton
public class RSConnect implements SessionInitHandler,
RSConnectActionEvent.Handler,
RSConnectDeployInitiatedEvent.Handler,
RSConnectDeploymentCompletedEvent.Handler,
RSConnectDeploymentFailedEvent.Handler
{
public interface Binder
extends CommandBinder<Commands, RSConnect> {}
@Inject
public RSConnect(EventBus events,
Commands commands,
Session session,
GlobalDisplay display,
DependencyManager dependencyManager,
Binder binder,
RSConnectServerOperations server,
SourceServerOperations sourceServer,
RPubsServerOperations rpubsServer,
RSAccountConnector connector,
Provider<UIPrefs> pUiPrefs,
PlotPublishMRUList plotMru)
{
commands_ = commands;
display_ = display;
dependencyManager_ = dependencyManager;
session_ = session;
server_ = server;
sourceServer_ = sourceServer;
rpubsServer_ = rpubsServer;
events_ = events;
connector_ = connector;
pUiPrefs_ = pUiPrefs;
plotMru_ = plotMru;
binder.bind(commands, this);
events.addHandler(SessionInitEvent.TYPE, this);
events.addHandler(RSConnectActionEvent.TYPE, this);
events.addHandler(RSConnectDeployInitiatedEvent.TYPE, this);
events.addHandler(RSConnectDeploymentCompletedEvent.TYPE, this);
events.addHandler(RSConnectDeploymentFailedEvent.TYPE, this);
// satellite windows don't get session init events, so initialize the
// session here
if (Satellite.isCurrentWindowSatellite())
{
ensureSessionInit();
}
exportNativeCallbacks();
}
@Override
public void onSessionInit(SessionInitEvent sie)
{
ensureSessionInit();
}
@Override
public void onRSConnectAction(final RSConnectActionEvent event)
{
// ignore if we're already waiting for a dependency check
if (depsPending_)
return;
// see if we have the requisite R packages
depsPending_ = true;
dependencyManager_.withRSConnect(
"Publishing content",
event.getContentType() == CONTENT_TYPE_DOCUMENT ||
event.getContentType() == CONTENT_TYPE_WEBSITE,
null, new CommandWithArg<Boolean>() {
@Override
public void execute(Boolean succeeded)
{
if (succeeded)
handleRSConnectAction(event);
depsPending_ = false;
}
});
}
private void publishAsRPubs(RSConnectActionEvent event)
{
String ctx = "Publish " + contentTypeDesc(event.getContentType());
RPubsUploadDialog dlg = new RPubsUploadDialog(
"Publish Wizard",
ctx,
event.getFromPreview() != null ?
event.getFromPreview().getSourceFile() : null,
event.getHtmlFile(),
event.getFromPrevious() == null ?
"" : event.getFromPrevious().getBundleId(),
false);
dlg.showModal();
}
private void showPublishUI(final RSConnectActionEvent event)
{
final RSConnectPublishInput input = new RSConnectPublishInput(event);
// set these inside the wizard input so we don't need to pass around
// session/prefs
input.setConnectUIEnabled(
pUiPrefs_.get().enableRStudioConnect().getGlobalValue());
input.setExternalUIEnabled(
session_.getSessionInfo().getAllowExternalPublish());
input.setDescription(event.getDescription());
if (event.getFromPrevious() != null)
{
switch (event.getContentType())
{
case CONTENT_TYPE_APP:
case CONTENT_TYPE_APP_SINGLE:
publishAsCode(event, null, true);
break;
case CONTENT_TYPE_PRES:
case CONTENT_TYPE_PLOT:
case CONTENT_TYPE_HTML:
case CONTENT_TYPE_DOCUMENT:
case CONTENT_TYPE_WEBSITE:
if (event.getFromPrevious().getServer().equals("rpubs.com"))
{
publishAsRPubs(event);
}
else
{
fillInputFromDoc(input, event.getPath(),
new CommandWithArg<RSConnectPublishInput>()
{
@Override
public void execute(RSConnectPublishInput arg)
{
if (arg == null)
return;
if (event.getFromPrevious().getAsStatic())
publishAsFiles(event,
new RSConnectPublishSource(event.getPath(),
event.getHtmlFile(),
arg.getWebsiteDir(),
arg.isSelfContained(),
true,
arg.isShiny(),
arg.getDescription(),
event.getContentType()));
else
publishAsCode(event, arg.getWebsiteDir(),
arg.isShiny());
}
});
}
break;
}
}
else
{
// plots and HTML are implicitly self-contained
if (event.getContentType() == CONTENT_TYPE_PLOT ||
event.getContentType() == CONTENT_TYPE_HTML ||
event.getContentType() == CONTENT_TYPE_PRES)
{
input.setIsSelfContained(true);
}
// if R Markdown, get info on what we're publishing from the server
if (event.getFromPreview() != null)
{
input.setSourceRmd(FileSystemItem.createFile(
event.getFromPreview().getSourceFile()));
fillInputFromDoc(input, event.getFromPreview().getSourceFile(),
new CommandWithArg<RSConnectPublishInput>()
{
@Override
public void execute(RSConnectPublishInput arg)
{
showPublishUI(arg);
}
});
}
else
{
showPublishUI(input);
}
}
}
private void showPublishUI(RSConnectPublishInput input)
{
final RSConnectActionEvent event = input.getOriginatingEvent();
if (input.getContentType() == CONTENT_TYPE_PLOT ||
input.getContentType() == CONTENT_TYPE_HTML ||
input.getContentType() == CONTENT_TYPE_PRES)
{
if (!input.isConnectUIEnabled() && input.isExternalUIEnabled())
{
publishAsRPubs(event);
}
else if (input.isConnectUIEnabled() && input.isExternalUIEnabled())
{
publishWithWizard(input);
}
else if (input.isConnectUIEnabled() && !input.isExternalUIEnabled())
{
publishAsStatic(input);
}
}
else if (input.getContentType() == CONTENT_TYPE_WEBSITE)
{
if (input.hasDocOutput())
{
publishWithWizard(input);
}
else
{
publishAsCode(event, input.getWebsiteDir(), false);
}
}
else if (input.getContentType() == CONTENT_TYPE_DOCUMENT)
{
if (input.isShiny())
{
if (input.isMultiRmd())
{
// multiple Shiny doc
publishWithWizard(input);
}
else
{
// single Shiny doc
publishAsCode(event, input.getWebsiteDir(), true);
}
}
else
{
if (input.isConnectUIEnabled())
{
if (input.hasDocOutput() ||
(input.isMultiRmd() && !input.isWebsiteRmd()))
{
// need to disambiguate between code/output and/or
// single/multi page
publishWithWizard(input);
}
else
{
// we don't have output, always publish the code
publishAsCode(event, input.getWebsiteDir(), false);
}
}
else if (input.isSelfContained() && input.hasDocOutput())
{
// RStudio Connect is disabled, go straight to RPubs
publishAsRPubs(event);
}
else
{
// we should generally hide the button in this case
display_.showErrorMessage("Content Not Publishable",
"Only self-contained documents can currently be " +
"published to RPubs.");
}
}
}
else if (input.getContentType() == CONTENT_TYPE_APP ||
input.getContentType() == CONTENT_TYPE_APP_SINGLE)
{
publishAsCode(event, null, true);
}
}
private void publishAsCode(RSConnectActionEvent event, String websiteDir,
boolean isShiny)
{
RSConnectPublishSource source = null;
if (event.getContentType() == CONTENT_TYPE_APP ||
event.getContentType() == CONTENT_TYPE_APP_SINGLE)
{
if (StringUtil.getExtension(event.getPath()).equalsIgnoreCase("r"))
{
FileSystemItem rFile = FileSystemItem.createFile(event.getPath());
// use the directory for the deployment record when publishing
// directory-based apps; use the file itself when publishing
// single-file apps
source = new RSConnectPublishSource(rFile.getParentPathString(),
event.getContentType() == CONTENT_TYPE_APP ?
rFile.getParentPathString() :
rFile.getName());
}
else
{
source = new RSConnectPublishSource(event.getPath(),
event.getPath());
}
}
else
{
source = new RSConnectPublishSource(event.getPath(), websiteDir,
false, false, isShiny, null, event.getContentType());
}
publishAsFiles(event, source);
}
private void publishAsStatic(RSConnectPublishInput input)
{
RSConnectPublishSource source = null;
if (input.getContentType() == RSConnect.CONTENT_TYPE_DOCUMENT ||
input.getContentType() == RSConnect.CONTENT_TYPE_WEBSITE)
{
source = new RSConnectPublishSource(
input.getOriginatingEvent().getFromPreview(),
input.getWebsiteDir(),
input.isSelfContained(),
true,
input.isShiny(),
input.getDescription());
}
else
{
source = new RSConnectPublishSource(
input.getOriginatingEvent().getHtmlFile(),
input.getWebsiteDir(),
input.isSelfContained(),
true,
input.isShiny(),
input.getDescription(),
input.getContentType());
}
publishAsFiles(input.getOriginatingEvent(), source);
}
private void publishAsFiles(RSConnectActionEvent event,
RSConnectPublishSource source)
{
RSConnectDeployDialog dialog =
new RSConnectDeployDialog(
event.getContentType(),
server_, this, display_,
source,
event.getFromPrevious());
dialog.showModal();
}
private void publishWithWizard(final RSConnectPublishInput input)
{
RSConnectPublishWizard wizard =
new RSConnectPublishWizard(input,
new ProgressOperationWithInput<RSConnectPublishResult>()
{
@Override
public void execute(RSConnectPublishResult result,
ProgressIndicator indicator)
{
switch (result.getPublishType())
{
case RSConnectPublishResult.PUBLISH_STATIC:
case RSConnectPublishResult.PUBLISH_CODE:
// always launch the browser--the wizard implies we're
// doing a first-time publish, and we may need to do some
// post-publish configuration
fireRSConnectPublishEvent(result, true);
indicator.onCompleted();
break;
case RSConnectPublishResult.PUBLISH_RPUBS:
uploadToRPubs(input, result, indicator);
break;
}
}
});
wizard.showModal();
}
@Override
public void onRSConnectDeployInitiated(
final RSConnectDeployInitiatedEvent event)
{
// shortcut: when deploying static content we don't need to do any linting
if (event.getSettings().getAsStatic())
{
doDeployment(event);
return;
}
// get lint results for the file or directory being deployed, as
// appropriate
server_.getLintResults(event.getSource().getDeployKey(),
new ServerRequestCallback<RSConnectLintResults>()
{
@Override
public void onResponseReceived(RSConnectLintResults results)
{
if (results.getErrorMessage().length() > 0)
{
display_.showYesNoMessage(GlobalDisplay.MSG_QUESTION,
"Lint Failed",
"The content you tried to publish could not be checked " +
"for errors. Do you want to proceed? \n\n" +
results.getErrorMessage(), false,
new ProgressOperation()
{
@Override
public void execute(ProgressIndicator indicator)
{
// "Publish Anyway"
doDeployment(event);
indicator.onCompleted();
}
},
new ProgressOperation()
{
@Override
public void execute(ProgressIndicator indicator)
{
// "Cancel"
indicator.onCompleted();
}
},
"Publish Anyway", "Cancel", false);
}
else if (results.hasLint())
{
display_.showYesNoMessage(GlobalDisplay.MSG_QUESTION,
"Publish Content Issues Found",
"Some issues were found in your content, which may " +
"prevent it from working correctly after publishing. " +
"Do you want to review these issues or publish anyway? "
, false,
new ProgressOperation()
{
@Override
public void execute(ProgressIndicator indicator)
{
// "Review Issues" -- we automatically show the
// markers so they're already behind the dialog.
indicator.onCompleted();
}
},
new ProgressOperation() {
@Override
public void execute(ProgressIndicator indicator)
{
// "Publish Anyway"
doDeployment(event);
indicator.onCompleted();
}
},
"Review Issues", "Publish Anyway", true);
}
else
{
// no lint and no errors -- good to go for deployment
doDeployment(event);
}
}
@Override
public void onError(ServerError error)
{
// we failed to lint, which is not encouraging, but we don't want to
// fail the whole deployment lest a balky linter prevent people from
// getting their work published, so forge on ahead.
doDeployment(event);
}
});
}
@Override
public void onRSConnectDeploymentCompleted(
RSConnectDeploymentCompletedEvent event)
{
if (launchBrowser_ && event.succeeded())
{
display_.openWindow(event.getUrl());
}
}
@Override
public void onRSConnectDeploymentFailed(
final RSConnectDeploymentFailedEvent event)
{
String failedPath = event.getData().getPath();
// if this looks like an API call, process the path to get the 'bare'
// server URL
int pos = failedPath.indexOf("__api__");
if (pos < 1)
{
// if not, just get the host
pos = failedPath.indexOf("/", 10) + 1;
}
if (pos > 0)
{
failedPath = failedPath.substring(0, pos);
}
final String serverUrl = failedPath;
new ModalDialogBase()
{
@Override
protected Widget createMainWidget()
{
setText("Publish Failed");
addOkButton(new ThemedButton("OK", new ClickHandler()
{
@Override
public void onClick(ClickEvent arg0)
{
closeDialog();
}
}));
HorizontalPanel panel = new HorizontalPanel();
Image errorImage =
new Image(new ImageResource2x(MessageDialogImages.INSTANCE.dialog_error2x()));
errorImage.getElement().getStyle().setMarginTop(1, Unit.EM);
errorImage.getElement().getStyle().setMarginRight(1, Unit.EM);
panel.add(errorImage);
panel.add(new HTML("<p>Your content could not be published because " +
"of a problem on the server.</p>" +
"<p>More information may be available on the server's home " +
"page:</p>" +
"<p><a href=\"" + serverUrl + "\">" + serverUrl + "</a>" +
"</p>" +
"<p>If the error persists, contact the server's " +
"administrator.</p>" +
"<p><small>Error code: " + event.getData().getHttpStatus() +
"</small></p>"));
return panel;
}
}.showModal();
}
public void ensureSessionInit()
{
if (sessionInited_)
return;
// "Manage accounts" can be invoked any time we're permitted to
// publish
commands_.rsconnectManageAccounts().setVisible(
SessionUtils.showPublishUi(session_, pUiPrefs_.get()));
// This object keeps track of the most recent deployment we made of each
// directory, and is used to default directory deployments to last-used
// settings.
new JSObjectStateValue(
"rsconnect",
"rsconnectDirectories",
ClientState.PERSISTENT,
session_.getSessionInfo().getClientState(),
false)
{
@Override
protected void onInit(JsObject value)
{
dirState_ = (RSConnectDirectoryState) (value == null ?
RSConnectDirectoryState.create() :
value.cast());
}
@Override
protected JsObject getValue()
{
dirStateDirty_ = false;
return (JsObject) (dirState_ == null ?
RSConnectDirectoryState.create().cast() :
dirState_.cast());
}
@Override
protected boolean hasChanged()
{
return dirStateDirty_;
}
};
sessionInited_ = true;
}
public static native void 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 launch,
JavaScriptObject record) /*-{
$wnd.opener.deployToRSConnect(sourceFile, deployDir, deployFile,
websiteDir, description, deployFiles,
additionalFiles, ignoredFiles, isSelfContained,
isShiny, asMultiple, asStatic, launch,
record);
}-*/;
public static boolean showRSConnectUI()
{
return true;
}
public static String contentTypeDesc(int contentType)
{
switch(contentType)
{
case RSConnect.CONTENT_TYPE_APP:
case RSConnect.CONTENT_TYPE_APP_SINGLE:
return "Application";
case RSConnect.CONTENT_TYPE_PLOT:
return "Plot";
case RSConnect.CONTENT_TYPE_HTML:
return "HTML";
case RSConnect.CONTENT_TYPE_DOCUMENT:
return "Document";
case RSConnect.CONTENT_TYPE_PRES:
return "Presentation";
case RSConnect.CONTENT_TYPE_WEBSITE:
return "Website";
}
return "Content";
}
public void fireRSConnectPublishEvent(RSConnectPublishResult result,
boolean launchBrowser)
{
if (Satellite.isCurrentWindowSatellite())
{
// in a satellite window, call back to the main window to do a
// deployment
RSConnect.deployFromSatellite(
result.getSource().getSourceFile(),
result.getSource().getDeployDir(),
result.getSource().getDeployFile(),
result.getSource().getWebsiteDir(),
result.getSource().getDescription(),
JsArrayUtil.toJsArrayString(
result.getSettings().getDeployFiles()),
JsArrayUtil.toJsArrayString(
result.getSettings().getAdditionalFiles()),
JsArrayUtil.toJsArrayString(
result.getSettings().getIgnoredFiles()),
result.getSource().isSelfContained(),
result.getSource().isShiny(),
result.getSettings().getAsMultiple(),
result.getSettings().getAsStatic(),
launchBrowser,
RSConnectDeploymentRecord.create(result.getAppName(),
result.getAppTitle(), result.getAccount(), ""));
// we can't raise the main window if we aren't in desktop mode, so show
// a dialog to guide the user there
if (!Desktop.isDesktop())
{
display_.showMessage(GlobalDisplay.MSG_INFO, "Deployment Started",
"RStudio is deploying " + result.getAppName() + ". " +
"Check the Deploy console tab in the main window for " +
"status updates. ");
}
}
else
{
// in the main window, initiate the deployment directly
events_.fireEvent(new RSConnectDeployInitiatedEvent(
result.getSource(),
result.getSettings(),
launchBrowser,
RSConnectDeploymentRecord.create(result.getAppName(),
result.getAppTitle(), result.getAccount(), "")));
}
}
// Private methods ---------------------------------------------------------
private void uploadToRPubs(RSConnectPublishInput input,
RSConnectPublishResult result,
final ProgressIndicator indicator)
{
RPubsUploader uploader = new RPubsUploader(rpubsServer_, display_,
events_, "rpubs-" + rpubsCount_++);
String contentType = contentTypeDesc(input.getContentType());
indicator.onProgress("Uploading " + contentType);
uploader.setOnUploadComplete(new CommandWithArg<Boolean>()
{
@Override
public void execute(Boolean arg)
{
indicator.onCompleted();
}
});
uploader.performUpload(contentType,
input.getSourceRmd() == null ? null :
input.getSourceRmd().getPath(),
input.getOriginatingEvent().getHtmlFile(),
input.getOriginatingEvent().getFromPrevious() == null ? "" :
input.getOriginatingEvent().getFromPrevious().getBundleId(),
false);
}
private void handleRSConnectAction(RSConnectActionEvent event)
{
if (event.getAction() == RSConnectActionEvent.ACTION_TYPE_DEPLOY)
{
// ignore this request if there's already a modal up
if (ModalDialogTracker.numModalsShowing() > 0)
return;
// show publish UI appropriate to the type of content being deployed
showPublishUI(event);
}
else if (event.getAction() == RSConnectActionEvent.ACTION_TYPE_CONFIGURE)
{
configureShinyApp(FilePathUtils.dirFromFile(event.getPath()));
}
}
private void doDeployment(final RSConnectDeployInitiatedEvent event)
{
server_.publishContent(event.getSource(),
event.getRecord().getAccountName(),
event.getRecord().getServer(),
event.getRecord().getName(),
event.getRecord().getTitle(),
event.getSettings(),
new ServerRequestCallback<Boolean>()
{
@Override
public void onResponseReceived(Boolean status)
{
if (status)
{
dirState_.addDeployment(event.getSource().getDeployDir(),
event.getRecord());
dirStateDirty_ = true;
if (event.getSource().getContentCategory() ==
RSConnect.CONTENT_CATEGORY_PLOT)
{
plotMru_.addPlotMruEntry(event.getRecord().getAccountName(),
event.getRecord().getServer(),
event.getRecord().getName(),
event.getRecord().getTitle());
}
launchBrowser_ = event.getLaunchBrowser();
events_.fireEvent(new RSConnectDeploymentStartedEvent(
event.getSource().isWebsiteRmd() ? "" :
event.getSource().getDeployKey(),
event.getSource().getDescription()));
}
else
{
display_.showErrorMessage("Deployment In Progress",
"Another deployment is currently in progress; only one " +
"deployment can be performed at a time.");
}
}
@Override
public void onError(ServerError error)
{
display_.showErrorMessage("Error Deploying Application",
"Could not deploy application '" +
event.getRecord().getName() +
"': " + error.getMessage());
}
});
}
// Manage, step 1: create a list of apps deployed from this directory
private void configureShinyApp(final String dir)
{
server_.getRSConnectDeployments(dir,
"",
new ServerRequestCallback<JsArray<RSConnectDeploymentRecord>>()
{
@Override
public void onResponseReceived(
JsArray<RSConnectDeploymentRecord> records)
{
configureShinyApp(dir, records);
}
@Override
public void onError(ServerError error)
{
display_.showErrorMessage("Error Configuring Application",
"Could not determine application deployments for '" +
dir + "':" + error.getMessage());
}
});
}
// Manage, step 2: Get the status of the applications from the server
private void configureShinyApp(final String dir,
JsArray<RSConnectDeploymentRecord> records)
{
if (records.length() == 0)
{
display_.showMessage(GlobalDisplay.MSG_INFO, "No Deployments Found",
"No application deployments were found for '" + dir + "'");
return;
}
// If we know the most recent deployment of the directory, act on that
// deployment by default
final ArrayList<RSConnectDeploymentRecord> recordList =
new ArrayList<RSConnectDeploymentRecord>();
RSConnectDeploymentRecord lastRecord = dirState_.getLastDeployment(dir);
if (lastRecord != null)
{
recordList.add(lastRecord);
}
for (int i = 0; i < records.length(); i++)
{
RSConnectDeploymentRecord record = records.get(i);
if (lastRecord == null)
{
recordList.add(record);
}
else
{
if (record.getUrl().equals(lastRecord.getUrl()))
recordList.set(0, record);
}
}
// We need to further filter the list by deployments that are
// eligible for termination (i.e. are currently running)
server_.getRSConnectAppList(recordList.get(0).getAccountName(),
recordList.get(0).getServer(),
new ServerRequestCallback<JsArray<RSConnectApplicationInfo>>()
{
@Override
public void onResponseReceived(JsArray<RSConnectApplicationInfo> apps)
{
configureShinyApp(dir, apps, recordList);
}
@Override
public void onError(ServerError error)
{
display_.showErrorMessage("Error Listing Applications",
error.getMessage());
}
});
}
// Manage, step 3: compare the deployments and apps active on the server
// until we find a running app from the current directory
private void configureShinyApp(String dir,
JsArray<RSConnectApplicationInfo> apps,
List<RSConnectDeploymentRecord> records)
{
for (int i = 0; i < records.size(); i++)
{
for (int j = 0; j < apps.length(); j++)
{
RSConnectApplicationInfo candidate = apps.get(j);
if (candidate.getName().equals(records.get(i).getName()))
{
// show the management ui
display_.openWindow(candidate.getConfigUrl());
return;
}
}
}
display_.showMessage(GlobalDisplay.MSG_INFO,
"No Running Deployments Found", "No applications deployed from '" +
dir + "' appear to be running.");
}
private final native void exportNativeCallbacks() /*-{
var thiz = this;
$wnd.deployToRSConnect = $entry(
function(sourceFile, deployDir, deployFile, websiteDir, description, deployFiles, additionalFiles, ignoredFiles, isSelfContained, isShiny, asMultiple, asStatic, launch, record) {
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;ZZZZZLcom/google/gwt/core/client/JavaScriptObject;)(sourceFile, deployDir, deployFile, websiteDir, description, deployFiles, additionalFiles, ignoredFiles, isSelfContained, isShiny, asMultiple, asStatic, launch, record);
}
);
}-*/;
private void 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 launch,
JavaScriptObject jsoRecord)
{
// this can be invoked by a satellite, so bring the main frame to the
// front if we can
if (Desktop.isDesktop())
Desktop.getFrame().bringMainFrameToFront();
else
WindowEx.get().focus();
ArrayList<String> deployFilesList =
JsArrayUtil.fromJsArrayString(deployFiles);
ArrayList<String> additionalFilesList =
JsArrayUtil.fromJsArrayString(additionalFiles);
ArrayList<String> ignoredFilesList =
JsArrayUtil.fromJsArrayString(ignoredFiles);
RSConnectDeploymentRecord record = jsoRecord.cast();
events_.fireEvent(new RSConnectDeployInitiatedEvent(
new RSConnectPublishSource(sourceFile, deployDir, deployFile,
websiteDir, isSelfContained, asStatic, isShiny, description),
new RSConnectPublishSettings(deployFilesList,
additionalFilesList, ignoredFilesList, asMultiple, asStatic),
launch, record));
}
private void fillInputFromDoc(final RSConnectPublishInput input,
final String docPath,
final CommandWithArg<RSConnectPublishInput> onComplete)
{
server_.getRmdPublishDetails(
docPath,
new ServerRequestCallback<RmdPublishDetails>()
{
@Override
public void onResponseReceived(RmdPublishDetails details)
{
input.setIsMultiRmd(details.isMultiRmd());
input.setIsShiny(details.isShinyRmd());
input.setIsSelfContained(details.isSelfContained());
input.setHasConnectAccount(details.hasConnectAccount());
input.setWebsiteDir(details.websiteDir());
if (StringUtil.isNullOrEmpty(input.getDescription()))
{
if (details.getTitle() != null &&
!details.getTitle().isEmpty())
{
// set the description from the document title, if we
// have it
input.setDescription(details.getTitle());
}
else
{
// set the description from the document name
input.setDescription(
FilePathUtils.fileNameSansExtension(docPath));
}
}
onComplete.execute(input);
}
@Override
public void onError(ServerError error)
{
// this is unlikely since the RPC does little work, but
// we can't offer the right choices in the wizard if we
// don't know what we're working with.
display_.showErrorMessage("Could Not Publish",
error.getMessage());
onComplete.execute(null);
}
});
}
private final Commands commands_;
private final GlobalDisplay display_;
private final Session session_;
private final RSConnectServerOperations server_;
private final RPubsServerOperations rpubsServer_;
private final SourceServerOperations sourceServer_;
private final DependencyManager dependencyManager_;
private final EventBus events_;
private final RSAccountConnector connector_;
private final Provider<UIPrefs> pUiPrefs_;
private final PlotPublishMRUList plotMru_;
private boolean launchBrowser_ = false;
private boolean sessionInited_ = false;
private boolean depsPending_ = false;
private String lastDeployedServer_ = "";
// incremented on each RPubs publish (to provide a unique context)
private static int rpubsCount_ = 0;
private RSConnectDirectoryState dirState_;
private boolean dirStateDirty_ = false;
public final static String CLOUD_SERVICE_NAME = "ShinyApps.io";
// No/unknown content type
public final static int CONTENT_TYPE_NONE = 0;
// A single HTML file representing a plot
public final static int CONTENT_TYPE_PLOT = 1;
// A document (.Rmd, .md, etc.),
public final static int CONTENT_TYPE_DOCUMENT = 2;
// A Shiny application
public final static int CONTENT_TYPE_APP = 3;
// A single-file Shiny application
public final static int CONTENT_TYPE_APP_SINGLE = 4;
// Standalone HTML (from HTML widgets/viewer pane, etc.)
public final static int CONTENT_TYPE_HTML = 5;
// A .Rpres presentation
public final static int CONTENT_TYPE_PRES = 6;
// A page in an R Markdown website
public final static int CONTENT_TYPE_WEBSITE = 7;
public final static String CONTENT_CATEGORY_PLOT = "plot";
public final static String CONTENT_CATEGORY_SITE = "site";
}