Skip to content

Commit

Permalink
Site/folder validation UI improvements (#5557)
Browse files Browse the repository at this point in the history
  • Loading branch information
labkey-adam authored Jun 1, 2024
1 parent e07ede3 commit 197eae2
Show file tree
Hide file tree
Showing 7 changed files with 372 additions and 105 deletions.
2 changes: 1 addition & 1 deletion api/src/org/labkey/api/view/JspTemplate.java
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public String render() throws Exception
context.setRequest(new MockHttpServletRequest("GET", null));
var mockResponse = new MockHttpServletResponse();
context.setResponse(mockResponse);
try (var init = HttpView.initForRequest(context, context.getRequest(), context.getResponse()))
try (var ignored = HttpView.initForRequest(context, context.getRequest(), context.getResponse()))
{
// Tomcat 8 Jasper rejects requests other than GET, POST, or HEAD... so, make this mock request a GET. #24750
include(this, out, context.getRequest(), context.getResponse());
Expand Down
1 change: 0 additions & 1 deletion core/src/org/labkey/core/CoreModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,6 @@
import org.labkey.api.thumbnail.ThumbnailService;
import org.labkey.api.usageMetrics.SimpleMetricsService;
import org.labkey.api.usageMetrics.UsageMetricsService;
import org.labkey.api.util.ConfigurationException;
import org.labkey.api.util.ContextListener;
import org.labkey.api.util.ExceptionUtil;
import org.labkey.api.util.FileUtil;
Expand Down
105 changes: 97 additions & 8 deletions core/src/org/labkey/core/admin/AdminController.java
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@
import org.labkey.api.writer.ZipUtil;
import org.labkey.bootstrap.ExplodedModuleService;
import org.labkey.core.admin.miniprofiler.MiniProfilerController;
import org.labkey.core.admin.sitevalidation.SiteValidationJob;
import org.labkey.core.admin.sql.SqlScriptController;
import org.labkey.core.portal.CollaborationFolderType;
import org.labkey.core.portal.ProjectController;
Expand Down Expand Up @@ -482,7 +483,6 @@ public static void registerManagementTabs()
addTab(TYPE.FolderManagement,"Files", "files", FOLDERS_AND_PROJECTS, FileRootsAction.class);
addTab(TYPE.FolderManagement,"Formats", "settings", FOLDERS_ONLY, FolderSettingsAction.class);
addTab(TYPE.FolderManagement,"Information", "info", NOT_ROOT, FolderInformationAction.class);
addTab(TYPE.FolderManagement,"Validate", "validate", EVERY_CONTAINER, ConfigureSiteValidationAction.class);
addTab(TYPE.FolderManagement,"R Config", "rConfig", NOT_ROOT, RConfigurationAction.class);

addTab(TYPE.ProjectSettings, "Properties", "properties", PROJECTS_ONLY, ProjectSettingsAction.class);
Expand Down Expand Up @@ -1495,10 +1495,10 @@ public HtmlString getSiteSettingsHelpLink(String fragment)
}

@RequiresPermission(AdminPermission.class)
public class ConfigureSiteValidationAction extends FolderManagementViewAction
public class ConfigureSiteValidationAction extends SimpleViewAction<Object>
{
@Override
protected JspView<?> getTabView()
public ModelAndView getView(Object o, BindException errors) throws Exception
{
return new JspView<>("/org/labkey/core/admin/sitevalidation/configureSiteValidation.jsp");
}
Expand All @@ -1513,8 +1513,19 @@ public void addNavTrail(NavTree root)

public static class SiteValidationForm
{
private boolean _includeSubfolders = false;
private List<String> _providers;
private boolean _includeSubfolders = false;
private transient Consumer<String> _logger = s -> {}; // No-op by default

public List<String> getProviders()
{
return _providers;
}

public void setProviders(List<String> providers)
{
_providers = providers;
}

public boolean isIncludeSubfolders()
{
Expand All @@ -1526,14 +1537,14 @@ public void setIncludeSubfolders(boolean includeSubfolders)
_includeSubfolders = includeSubfolders;
}

public List<String> getProviders()
public Consumer<String> getLogger()
{
return _providers;
return _logger;
}

public void setProviders(List<String> providers)
public void setLogger(Consumer<String> logger)
{
_providers = providers;
_logger = logger;
}
}

Expand All @@ -1554,6 +1565,84 @@ public void addNavTrail(NavTree root)
}
}

@RequiresPermission(AdminPermission.class)
public static class SiteValidationBackgroundAction extends FormHandlerAction<SiteValidationForm>
{
private ActionURL _redirectUrl;

@Override
public void validateCommand(SiteValidationForm form, Errors errors)
{
}

@Override
public boolean handlePost(SiteValidationForm form, BindException errors) throws PipelineValidationException
{
ViewBackgroundInfo vbi = new ViewBackgroundInfo(getContainer(), getUser(), null);
PipeRoot root = PipelineService.get().findPipelineRoot(getContainer());
SiteValidationJob job = new SiteValidationJob(vbi, root, form);
PipelineService.get().queueJob(job);
String jobGuid = job.getJobGUID();

if (null == jobGuid)
throw new NotFoundException("Unable to determine pipeline job GUID");

Integer jobId = PipelineService.get().getJobId(getUser(), getContainer(), jobGuid);

if (null == jobId)
throw new NotFoundException("Unable to determine pipeline job ID");

PipelineStatusUrls urls = urlProvider(PipelineStatusUrls.class);
_redirectUrl = urls.urlDetails(getContainer(), jobId);

return true;
}

@Override
public URLHelper getSuccessURL(SiteValidationForm form)
{
return _redirectUrl;
}
}

public static class ViewValidationResultsForm
{
private String _fileName;

public String getFileName()
{
return _fileName;
}

@SuppressWarnings("unused")
public void setFileName(String fileName)
{
_fileName = fileName;
}
}

@RequiresPermission(AdminPermission.class)
public class ViewValidationResultsAction extends SimpleViewAction<ViewValidationResultsForm>
{
@Override
public ModelAndView getView(ViewValidationResultsForm form, BindException errors) throws Exception
{
PipeRoot root = PipelineService.get().findPipelineRoot(getContainer());
File results = new File(root.getLogDirectory(), form.getFileName());
if (!results.isFile())
throw new NotFoundException("File not found: " + form.getFileName());

return new HtmlView(HtmlString.unsafe(PageFlowUtil.getFileContentsAsString(results)));
}

@Override
public void addNavTrail(NavTree root)
{
setHelpTopic("siteValidation");
addAdminNavTrail(root, "View " + (getContainer().isRoot() ? "Site" : "Folder") + " Validation Results", getClass());
}
}

public interface FileManagementForm
{
String getFolderRootPath();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package org.labkey.core.admin.sitevalidation;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.labkey.api.pipeline.PipeRoot;
import org.labkey.api.pipeline.PipelineJob;
import org.labkey.api.util.FileUtil;
import org.labkey.api.util.StringUtilsLabKey;
import org.labkey.api.util.URLHelper;
import org.labkey.api.view.ActionURL;
import org.labkey.api.view.JspTemplate;
import org.labkey.api.view.ViewBackgroundInfo;
import org.labkey.api.view.ViewContext;
import org.labkey.core.admin.AdminController.SiteValidationForm;
import org.labkey.core.admin.AdminController.ViewValidationResultsAction;

import java.io.File;
import java.io.PrintWriter;

public class SiteValidationJob extends PipelineJob
{
private final SiteValidationForm _form;

@JsonCreator
protected SiteValidationJob(@JsonProperty("_form") SiteValidationForm form)
{
_form = form;
}

public SiteValidationJob(ViewBackgroundInfo info, PipeRoot pipeRoot, SiteValidationForm form)
{
super("SiteValidation", info, pipeRoot);
setLogFile(new File(pipeRoot.getLogDirectory(), FileUtil.makeFileNameWithTimestamp("site_validation", "log")).toPath());
_form = form;
}

@Override
public URLHelper getStatusHref()
{
return new ActionURL(ViewValidationResultsAction.class, getContainer())
.addParameter("fileName", getResultsFileName());
}

@Override
public String getDescription()
{
return "Site Validation";
}

@Override
public void run()
{
info("Site validation started");
PipelineJob.TaskStatus finalStatus = PipelineJob.TaskStatus.complete;
_form.setLogger(s -> {
getLogger().info(s);
setStatus(s);
});
JspTemplate<SiteValidationForm> template = new JspTemplate<>("/org/labkey/core/admin/sitevalidation/siteValidation.jsp", _form);
ViewContext context = new ViewContext(getInfo());
template.setViewContext(context);
File results = new File(getPipeRoot().getLogDirectory(), getResultsFileName());

try (PrintWriter out = new PrintWriter(results, StringUtilsLabKey.DEFAULT_CHARSET))
{
out.println(template.render());
}
catch (Exception e)
{
getLogger().error("Site validation failed", e);
finalStatus = TaskStatus.error;
}

info("Site validation complete");
setStatus(finalStatus);
}

private String getResultsFileName()
{
return getLogFile().getName().replace(".log", ".html");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
<%@ page import="org.labkey.api.admin.sitevalidation.SiteValidationService" %>
<%@ page import="org.labkey.api.util.DOM" %>
<%@ page import="org.labkey.api.util.HtmlString" %>
<%@ page import="org.labkey.core.admin.AdminController" %>
<%@ page import="org.labkey.core.admin.AdminController.SiteValidationAction" %>
<%@ page import="org.labkey.core.admin.AdminController.SiteValidationBackgroundAction" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.util.Collection" %>
<%@ page import="java.util.List" %>
Expand Down Expand Up @@ -67,7 +68,7 @@
{
%>
Clicking the "Validate" button will run the selected validators in the designated folder(s). Producing the results could take some time, especially with many folders, providers, and/or objects that need to be validated.<br><br>
<labkey:form action="<%=urlFor(AdminController.SiteValidationAction.class)%>" method="post">
<labkey:form id="form" action="<%=urlFor(SiteValidationAction.class)%>" method="get">
<%
if (getContainer().isRoot())
renderProviderList("Site Validation Providers", validationService.getSiteProviders(), out);
Expand All @@ -91,17 +92,37 @@ Clicking the "Validate" button will run the selected validators in the designate
{
out.println(
DOM.createHtmlFragment(
input().type("checkbox").checked(true).name("includeSubfolders"),
input().type("checkbox").checked(true).value("true").name("includeSubfolders"),
"Include subfolders",
DOM.BR(),
DOM.BR()
)
);
}
out.println(
DOM.createHtmlFragment(
input().id("background").type("checkbox").checked(false).value("true").name("background").onChange("change()"),
"Run in the background",
helpPopup("Validating many folders can take a long time. Running in a background pipeline job avoids proxy timeouts. Once the job completes, click the \"Data\" button to see the report."),
DOM.BR(),
DOM.BR()
)
);
%>
<%=button("Validate").usePost().submit(true)%>
<%=button("Validate").submit(true)%>
<%=generateBackButton("Cancel")%>
<labkey:csrf/>
</labkey:form>
<%
}
%>
%>
<script type="text/javascript" nonce="<%=getScriptNonce()%>">
// We want to GET SiteValidationAction for foreground render (keep parameters on the URL for link sharing)
// or POST to SiteValidationBackgroundAction for background render (handle CSRF and mutating SQL)
function change()
{
const background = document.getElementById("background").checked;
const form = document.getElementById("form");
form.action = background ? <%=q(urlFor(SiteValidationBackgroundAction.class))%> : <%=q(urlFor(SiteValidationAction.class))%>;
form.method = background ? "post" : "get";
}
</script>
Loading

0 comments on commit 197eae2

Please sign in to comment.