/*
* Copyright (c) 2015 EMC Corporation
* All Rights Reserved
*/
package controllers.catalog;
import static com.emc.vipr.client.core.util.ResourceUtils.id;
import static com.emc.vipr.client.core.util.ResourceUtils.uri;
import static util.BourneUtil.getCatalogClient;
import static util.BourneUtil.getViprClient;
import java.net.URI;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.joda.time.DateTime;
import com.emc.sa.util.TextUtils;
import com.emc.storageos.db.client.model.uimodels.OrderStatus;
import com.emc.storageos.model.TaskResourceRep;
import com.emc.storageos.model.search.SearchResultResourceRep;
import com.emc.storageos.model.search.Tags;
import com.emc.vipr.client.ViPRCoreClient;
import com.emc.vipr.client.core.impl.TaskUtil;
import com.emc.vipr.model.catalog.ApprovalRestRep;
import com.emc.vipr.model.catalog.CatalogServiceRestRep;
import com.emc.vipr.model.catalog.ExecutionLogRestRep;
import com.emc.vipr.model.catalog.ExecutionStateRestRep;
import com.emc.vipr.model.catalog.OrderCreateParam;
import com.emc.vipr.model.catalog.OrderLogRestRep;
import com.emc.vipr.model.catalog.OrderRestRep;
import com.emc.vipr.model.catalog.Parameter;
import com.emc.vipr.model.catalog.ScheduledEventCreateParam;
import com.emc.vipr.model.catalog.ScheduledEventRestRep;
import com.emc.vipr.model.catalog.ServiceDescriptorRestRep;
import com.emc.vipr.model.catalog.ServiceFieldRestRep;
import com.emc.vipr.model.catalog.ServiceFieldTableRestRep;
import com.emc.vipr.model.catalog.ServiceItemRestRep;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import controllers.Common;
import controllers.Tasks;
import controllers.Tasks.WorkflowStep;
import controllers.deadbolt.Restrict;
import controllers.deadbolt.Restrictions;
import controllers.resources.AffectedResources;
import controllers.resources.AffectedResources.ResourceDetails;
import controllers.security.Security;
import controllers.tenant.TenantSelector;
import controllers.util.FlashException;
import controllers.util.Models;
import models.BreadCrumb;
import models.datatable.OrderDataTable;
import models.datatable.OrderDataTable.OrderInfo;
import models.datatable.RecentOrdersDataTable;
import play.Logger;
import play.data.binding.As;
import play.data.validation.Required;
import play.data.validation.Validation;
import play.mvc.Http;
import play.mvc.Util;
import play.mvc.With;
import util.BourneUtil;
import util.CatalogServiceUtils;
import util.MessagesUtils;
import util.ModelExtensions;
import util.OrderUtils;
import util.StringOption;
import util.TagUtils;
import util.api.ApiMapperUtils;
import util.datatable.DataTableParams;
import util.datatable.DataTablesSupport;
@With(Common.class)
public class Orders extends OrderExecution {
private static final int SHORT_DELAY = 1000;
private static final int NORMAL_DELAY = 3000;
private static final int LONG_DELAY = 15000;
private static final int DEFAULT_DELAY = 60000;
private static final int RECEIPT_UPDATE_ATTEMPTS = 5;
public static final String RECENT_ACTIVITIES = "VIPRUI_RECENT_ACTIVITIES";
public static final int MAX_RECENT_SERVICES = 4;
private static void addMaxDaysRenderArgs() {
Integer maxDays = params.get("maxDays", Integer.class);
if (maxDays == null) {
if (StringUtils.isNotEmpty(params.get("startDate"))
&& StringUtils.isNotEmpty(params.get("endDate"))) {
renderArgs.put("startDate", params.get("startDate"));
renderArgs.put("endDate", params.get("endDate"));
maxDays = 0;
} else {
maxDays = 1;
}
}
int[] days = { 1, 7, 14, 30, 90, 0 };
List<StringOption> options = Lists.newArrayList();
options.add(new StringOption(String.valueOf(maxDays), MessagesUtils.get("orders.nDays", maxDays)));
for (int day : days) {
if (day == maxDays) {
options.remove(0);
}
options.add(new StringOption(String.valueOf(day), MessagesUtils.get("orders." + day + "days")));
}
renderArgs.put("offsetInMinutes", params.get("offsetInMinutes"));
renderArgs.put("maxDays", maxDays);
renderArgs.put("dateDaysAgo", OrderDataTable.getDateDaysAgo(maxDays));
renderArgs.put("maxDaysOptions", options);
}
@Restrictions({ @Restrict("TENANT_ADMIN") })
public static void allOrders() {
//TODO should get client time zone from request, currently we get it from javascript method
RecentUserOrdersDataTable dataTable = new RecentUserOrdersDataTable();
TenantSelector.addRenderArgs();
dataTable.setByStartEndDateOrMaxDays(params.get("startDate"), params.get("endDate"),
params.get("maxDays", Integer.class));
Long orderCount = dataTable.fetchCount().getCounts().get(Models.currentAdminTenant());
renderArgs.put("orderCount", orderCount);
if (orderCount > OrderDataTable.ORDER_MAX_COUNT) {
flash.put("warning", MessagesUtils.get("orders.warning", orderCount, OrderDataTable.ORDER_MAX_COUNT));
}
String deleteStatus = dataTable.getDeleteJobStatus();
if (deleteStatus != null) {
flash.put("info", deleteStatus);
renderArgs.put("disableDeleteAllAndDownload", 1);
} else {
String downloadStatus = dataTable.getDownloadJobStatus();
if (downloadStatus != null) {
flash.put("info", downloadStatus);
renderArgs.put("disableDeleteAllAndDownload", 1);
}
}
renderArgs.put("canBeDeletedStatuses", RecentOrdersDataTable.getCanBeDeletedOrderStatuses());
addMaxDaysRenderArgs();
Common.copyRenderArgsToAngular();
render(dataTable);
}
@Restrictions({ @Restrict("TENANT_ADMIN") })
public static void allOrdersJson(Integer maxDays) {
DataTableParams dataTableParams = DataTablesSupport.createParams(params);
RecentUserOrdersDataTable dataTable = new RecentUserOrdersDataTable();
dataTable.setByStartEndDateOrMaxDays(params.get("startDate"), params.get("endDate"), maxDays);
renderJSON(DataTablesSupport.createJSON(dataTable.fetchData(dataTableParams), params));
}
public static void list() {
OrderDataTable dataTable = new OrderDataTable(Models.currentTenant(), NumberUtils.toInt(params.get("offsetInMinutes"), 0));
dataTable.setUserInfo(Security.getUserInfo());
dataTable.setByStartEndDateOrMaxDays(params.get("startDate"), params.get("endDate"),
params.get("maxDays", Integer.class));
Long orderCount = dataTable.fetchCount().getCounts().entrySet().iterator().next().getValue();
if (orderCount > OrderDataTable.ORDER_MAX_COUNT) {
flash.put("warning", MessagesUtils.get("orders.warning", orderCount, OrderDataTable.ORDER_MAX_COUNT));
}
addMaxDaysRenderArgs();
Common.copyRenderArgsToAngular();
render(dataTable);
}
public static void listJson() {
OrderDataTable dataTable = new OrderDataTable(Models.currentTenant(), NumberUtils.toInt(params.get("offsetInMinutes"), 0));
dataTable.setUserInfo(Security.getUserInfo());
dataTable.setByStartEndDateOrMaxDays(params.get("startDate"), params.get("endDate"),
params.get("maxDays", Integer.class));
renderJSON(DataTablesSupport.createJSON(dataTable.fetchAll(), params));
}
public static void itemsJson(@As(",") String[] ids) {
List<OrderInfo> results = Lists.newArrayList();
if (ids != null && ids.length > 0) {
for (String id : ids) {
if (StringUtils.isNotBlank(id)) {
OrderRestRep order = OrderUtils.getOrder(uri(id));
if (order != null) {
Models.checkAccess(order.getTenant());
results.add(new OrderInfo(order));
}
}
}
}
renderJSON(results);
}
@FlashException(value = "allOrders")
@Restrictions({ @Restrict("TENANT_ADMIN") })
public static void deleteOrders(@As(",") String[] ids) {
if (ids != null && ids.length > 0) {
List<URI> uris = Lists.newArrayList();
for (String id : ids) {
uris.add(uri(id));
}
OrderUtils.deactivateOrders(uris);
} else {
RecentUserOrdersDataTable dataTable = new RecentUserOrdersDataTable();
dataTable.setByStartEndDateOrMaxDays(params.get("startDate"), params.get("endDate"), params.get("maxDays", Integer.class));
dataTable.deleteOrders();
}
flash.success(MessagesUtils.get("orders.delete.submitted"));
redirect(Common.toSafeRedirectURL("/catalog.orders/allorders?"+request.querystring));
}
@FlashException(value = "allOrders")
@Restrictions({ @Restrict("TENANT_ADMIN") })
public static void downloadOrders(String ids) {
RecentUserOrdersDataTable dataTable = new RecentUserOrdersDataTable();
dataTable.downloadOrders(params.get("startDate"), params.get("endDate"), params.get("maxDays", Integer.class), ids);
}
@FlashException(referrer = { "receiptContent" })
public static void rollbackTask(String orderId, String taskId) {
if (StringUtils.isNotBlank(taskId)) {
ViPRCoreClient client = BourneUtil.getViprClient();
client.tasks().rollback(uri(taskId));
flash.put("info", MessagesUtils.get("resources.tasks.rollbackMessage", taskId));
}
receipt(orderId);
}
@FlashException(referrer = { "receiptContent" })
public static void retryTask(String orderId, String taskId) {
if (StringUtils.isNotBlank(taskId)) {
ViPRCoreClient client = BourneUtil.getViprClient();
client.tasks().resume(uri(taskId));
flash.put("info", MessagesUtils.get("resources.tasks.retryMessage", taskId));
}
receipt(orderId);
}
@FlashException(referrer = { "receiptContent" })
public static void resumeTask(String orderId, String taskId) {
if (StringUtils.isNotBlank(taskId)) {
ViPRCoreClient client = BourneUtil.getViprClient();
client.tasks().resume(uri(taskId));
flash.put("info", MessagesUtils.get("resources.tasks.resumeMessage", taskId));
}
receipt(orderId);
}
/**
* Resubmits an order, creating a new copy with the same parameters.
*
* @param orderId
* the order ID.
*/
public static void resubmitOrder(@Required String orderId) {
checkAuthenticity();
OrderRestRep order = OrderUtils.getOrder(uri(orderId));
addParametersToFlash(order);
Services.showForm(order.getCatalogService().getId().toString());
}
@Util
private static void addParametersToFlash(OrderRestRep order) {
CatalogServiceRestRep service = CatalogServiceUtils.getCatalogService(uri(order.getCatalogService().getId().toString()));
HashMap<String, String> tableParams = new HashMap<String, String>();
for (ServiceItemRestRep item : service.getServiceDescriptor().getItems()) {
if (item.isTable()) {
for (ServiceFieldRestRep tableItem : ((ServiceFieldTableRestRep) item).getItems()) {
tableParams.put(tableItem.getName(), item.getName());
}
}
}
for (Parameter parameter : order.getParameters()) {
// Do not add encrypted values to the flash scope
if (parameter.isEncrypted()) {
continue;
}
List<String> values = TextUtils.parseCSV(parameter.getValue());
for (int i = 0; i < values.size(); i++) {
String value = values.get(i);
String name = parameter.getLabel();
if (tableParams.containsKey(name)) {
name = tableParams.get(name) + "[" + i + "]." + name;
}
flash.put(name, value);
}
}
}
public static void submitOrder(String serviceId) {
checkAuthenticity();
OrderCreateParam order = createAndValidateOrder(serviceId);
String status = null;
String orderId = null;
try {
if (isSchedulerEnabled()) {
ScheduledEventCreateParam event = createScheduledOrder(order);
if (Validation.hasErrors()) {
Validation.keep();
Common.flashParamsExcept("json", "body");
Services.showForm(serviceId);
}
ScheduledEventRestRep submittedEvent = getCatalogClient().orders().submitScheduledEvent(event);
status = submittedEvent.getEventStatus();
orderId = submittedEvent.getLatestOrderId().toString();
} else {
OrderRestRep submittedOrder = getCatalogClient().orders().submit(order);
status = submittedOrder.getOrderStatus();
orderId = submittedOrder.getId().toString();
}
} catch (Exception e) {
Logger.error(e, MessagesUtils.get("order.submitFailedWithDetail", e.getMessage()));
flash.error(MessagesUtils.get("order.submitFailedWithDetail", e.getMessage()));
Common.handleError();
}
if (OrderRestRep.ERROR.equalsIgnoreCase(status)) {
flash.error(MessagesUtils.get("order.submitFailed"));
} else {
flash.success(MessagesUtils.get("order.submitSuccess"));
}
Http.Cookie cookie = request.cookies.get(RECENT_ACTIVITIES);
response.setCookie(RECENT_ACTIVITIES, updateRecentActivitiesCookie(cookie, serviceId));
receipt(orderId);
}
@Util
public static OrderCreateParam createAndValidateOrder(String serviceId) {
CatalogServiceRestRep service = CatalogServiceUtils.getCatalogService(uri(serviceId));
ServiceDescriptorRestRep descriptor = service.getServiceDescriptor();
// Filter out actual Service Parameters
Map<String, String> parameters = parseParameters(service, descriptor);
if (Validation.hasErrors()) {
Validation.keep();
Common.flashParamsExcept("json", "body");
Services.showForm(serviceId);
}
return createOrder(service, descriptor, parameters);
}
public static void receipt(String orderId) {
OrderDetails details = new OrderDetails(orderId);
Models.checkAccess(details.order.getTenant());
fetchData(details);
ServiceDescriptorRestRep descriptor = details.catalogService.getServiceDescriptor();
addBreadCrumbToRenderArgs(id(details.order.getTenant()), details.catalogService);
render(orderId, details, descriptor);
}
private static void addBreadCrumbToRenderArgs(URI tenant, CatalogServiceRestRep service) {
List<BreadCrumb> breadcrumbs = ServiceCatalog.createBreadCrumbs(tenant.toString(), service);
renderArgs.put("breadcrumbs", breadcrumbs);
}
public static void receiptContent(String orderId, Long lastUpdated) {
OrderDetails details = waitForUpdatedOrder(orderId, lastUpdated);
Models.checkAccess(details.order.getTenant());
fetchData(details);
render(orderId, details);
}
/**
* Waits for an update to the order. The lastUpdated value is specified by the receipt page and only once the
* order has been updated more recently than that are the detail returned.
*
* @param orderId the order ID.
* @param lastUpdated the last updated time.
* @return the order details.
*/
private static OrderDetails waitForUpdatedOrder(String orderId, Long lastUpdated) {
if (lastUpdated == null) {
Logger.debug("No last updated value");
return new OrderDetails(orderId);
}
Map<URI, String> oldTasksStateMap = null;
int updateAttempts = 0;
// Wait for an update to the order
while (true) {
OrderDetails details = new OrderDetails(orderId);
if (details.isNewer(lastUpdated)) {
Logger.debug("Found update for order %s newer than: %s", details.order.getOrderNumber(), lastUpdated);
return details;
}
if (details.isFinished()) {
Logger.debug("Found finished order %s", details.order.getOrderNumber());
return details;
}
if (oldTasksStateMap != null && details.viprTasks != null) {
if (isTaskStateChanged(oldTasksStateMap, details.viprTasks)) {
Logger.debug("Found task state change for order %s", details.order.getOrderNumber());
return details;
}
} else {
oldTasksStateMap = createTaskStateMap(details.viprTasks);
}
if (++updateAttempts >= RECEIPT_UPDATE_ATTEMPTS) {
Logger.debug("Updating order %s after %d attempts to find order change", details.order.getOrderNumber(),
RECEIPT_UPDATE_ATTEMPTS);
return details;
}
// Pause and check again, delay is based on order state
int delay = getWaitDelay(details);
Logger.debug("No update for order %s, waiting for %s ms", details.order.getOrderNumber(), delay);
await(delay);
}
}
private static Map<URI, String> createTaskStateMap(List<TaskResourceRep> tasks) {
Map<URI, String> taskMap = Maps.newHashMap();
for (TaskResourceRep task : tasks) {
taskMap.put(task.getId(), task.getState());
}
return taskMap;
}
private static boolean isTaskStateChanged(Map<URI, String> oldTasksStateMap, List<TaskResourceRep> viprTasks) {
Map<URI, String> currentTaskStateMap = createTaskStateMap(viprTasks);
return !Maps.difference(oldTasksStateMap, currentTaskStateMap).areEqual();
}
private static int getWaitDelay(OrderDetails details) {
OrderStatus status = OrderStatus.valueOf(details.order.getOrderStatus());
switch (status) {
case PENDING:
case APPROVED:
// Pending and approved will be quick transitions
return SHORT_DELAY;
case EXECUTING:
// Order is executing, normal delay
return NORMAL_DELAY;
case SCHEDULED:
case APPROVAL:
// Order is waiting, long delay
return LONG_DELAY;
default:
return DEFAULT_DELAY;
}
}
private static String updateRecentActivitiesCookie(Http.Cookie cookie, String serviceId) {
List<String> ids = Lists.newArrayList();
if (cookie != null && cookie.value != null) {
ids.addAll(Arrays.asList(cookie.value.split(",")));
if (ids.contains(serviceId)) {
ids.remove(serviceId);
}
}
ids.add(0, serviceId);
while (ids.size() > MAX_RECENT_SERVICES) {
ids.remove(ids.size() - 1);
}
return StringUtils.join(ids, ",");
}
/**
* Fetches the remaining data for the order.
*/
protected static void fetchData(OrderDetails details) {
details.catalogService = CatalogServiceUtils.getCatalogService(details.order.getCatalogService());
if (details.executionState != null) {
details.affectedResources = Lists.newArrayList();
for (String affectedResourceId : details.executionState.getAffectedResources()) {
ResourceDetails resourceDetails = AffectedResources.resourceDetails(affectedResourceId);
if (resourceDetails != null) {
details.affectedResources.add(resourceDetails);
}
}
Collections.sort(details.affectedResources, RESOURCE_COMPARATOR);
}
}
public static class OrderDetails {
public Long lastUpdated;
public OrderRestRep order;
public ApprovalRestRep approval;
public CatalogServiceRestRep catalogService;
public List<Parameter> orderParameters;
public ExecutionStateRestRep executionState;
public List<OrderLogRestRep> logs;
public List<ExecutionLogRestRep> precheckTaskLogs;
public List<ExecutionLogRestRep> executeTaskLogs;
public List<ExecutionLogRestRep> rollbackTaskLogs;
public List<ResourceDetails> affectedResources;
public Tags tags;
public List<TaskResourceRep> viprTasks;
public ScheduledEventRestRep scheduledEvent;
public Date scheduleStartDateTime;
public Map<URI, String> viprTaskStepMessages;
public Map<URI, List<String>> viprTaskWarningMessages;
public OrderDetails(String orderId) {
order = OrderUtils.getOrder(uri(orderId));
orderParameters = order.getParameters();
if (order == null) {
return;
}
checkLastUpdated(order);
approval = getCatalogClient().approvals().search().byOrderId(uri(orderId)).first();
checkLastUpdated(approval);
tags = ApiMapperUtils.getTags(order);
ViPRCoreClient client = getViprClient();
List<SearchResultResourceRep> searchResults = client.tasks().performSearchBy("tag", TagUtils.createOrderIdTag(orderId));
viprTasks = client.tasks().getByRefs(searchResults);
setTaskStepMessages();
setTaskWarningMessages();
checkLastUpdated(viprTasks);
executionState = OrderUtils.getExecutionState(order.getId());
if (executionState != null) {
checkLastUpdated(executionState);
logs = OrderUtils.getOrderLogs(order.getId());
List<ExecutionLogRestRep> taskLogs = OrderUtils.getExecutionLogs(order.getId());
precheckTaskLogs = Lists.newArrayList();
executeTaskLogs = Lists.newArrayList();
rollbackTaskLogs = Lists.newArrayList();
for (ExecutionLogRestRep log : taskLogs) {
if (ModelExtensions.isPrecheck(log)) {
precheckTaskLogs.add(log);
} else if (ModelExtensions.isExecute(log)) {
executeTaskLogs.add(log);
} else if (ModelExtensions.isRollback(log)) {
rollbackTaskLogs.add(log);
}
checkLastUpdated(log);
}
}
URI scheduledEventId = order.getScheduledEventId();
if (scheduledEventId != null) {
scheduledEvent = getCatalogClient().orders().getScheduledEvent(scheduledEventId);
String isoDateTimeStr = String.format("%sT%02d:%02d:00Z",
scheduledEvent.getScheduleInfo().getStartDate(),
scheduledEvent.getScheduleInfo().getHourOfDay(),
scheduledEvent.getScheduleInfo().getMinuteOfHour());
DateTime startDateTime = DateTime.parse(isoDateTimeStr);
scheduleStartDateTime = startDateTime.toDate();
}
}
private void setTaskWarningMessages() {
viprTaskWarningMessages = Maps.newHashMap();
for (TaskResourceRep task : viprTasks) {
if (task != null && task.getWarningMessages() != null && !task.getWarningMessages().isEmpty()) {
viprTaskWarningMessages.put(task.getId(), task.getWarningMessages());
}
}
}
private void setTaskStepMessages() {
viprTaskStepMessages = Maps.newHashMap();
for (TaskResourceRep task : viprTasks) {
if (task.getWorkflow() != null && TaskUtil.isSuspended(task)) {
List<WorkflowStep> steps = Tasks.getWorkflowSteps(task.getWorkflow().getId());
String message = "";
for (WorkflowStep step : steps) {
if (TaskUtil.isSuspended(task) && step.isSuspended()) {
message += step.message;
}
}
viprTaskStepMessages.put(task.getId(), message);
}
}
}
private void checkLastUpdated(OrderRestRep obj) {
if ((obj != null) && (obj.getLastUpdated() != null)) {
long updated = obj.getLastUpdated().getTime();
if ((lastUpdated == null) || (lastUpdated < updated)) {
lastUpdated = updated;
}
}
}
private void checkLastUpdated(ExecutionStateRestRep obj) {
if ((obj != null) && (obj.getLastUpdated() != null)) {
long updated = obj.getLastUpdated().getTime();
if ((lastUpdated == null) || (lastUpdated < updated)) {
lastUpdated = updated;
}
}
}
private void checkLastUpdated(ExecutionLogRestRep obj) {
if ((obj != null) && (obj.getLastUpdated() != null)) {
long updated = obj.getLastUpdated().getTime();
if ((lastUpdated == null) || (lastUpdated < updated)) {
lastUpdated = updated;
}
}
}
private void checkLastUpdated(ApprovalRestRep approval) {
if ((approval != null) && (approval.getDateActioned() != null)) {
long updated = approval.getDateActioned().getTime();
if ((lastUpdated == null) || (lastUpdated < updated)) {
lastUpdated = updated;
}
}
}
private void checkLastUpdated(List<TaskResourceRep> tasks) {
for (TaskResourceRep task : tasks) {
if ((task != null) && (task.getStartTime() != null)) {
long updated = task.getStartTime().getTimeInMillis();
if ((lastUpdated == null) || (lastUpdated < updated)) {
lastUpdated = updated;
}
}
if ((task != null) && (task.getEndTime() != null)) {
long updated = task.getEndTime().getTimeInMillis();
if ((lastUpdated == null) || (lastUpdated < updated)) {
lastUpdated = updated;
}
}
}
}
/**
* Determines if this has been updated more recently than the given time.
*
* @param time
* the time.
* @return true if this has been updated more recently.
*/
public boolean isNewer(long time) {
return (lastUpdated != null) && (lastUpdated > time);
}
/**
* Determines if the order is in a finished state (whether successful or not).
*
* @return true if the order is finished.
*/
public boolean isFinished() {
try {
OrderStatus status = OrderStatus.valueOf(order.getOrderStatus());
switch (status) {
case CANCELLED:
case PARTIAL_SUCCESS:
case REJECTED:
case SUCCESS:
case ERROR:
return true;
default:
return false;
}
} catch (RuntimeException e) {
return false;
}
}
public ScheduledEventRestRep getScheduledEvent() {
return scheduledEvent;
}
}
public static final Comparator<ResourceDetails> RESOURCE_COMPARATOR = new Comparator<ResourceDetails>() {
@Override
public int compare(ResourceDetails o1, ResourceDetails o2) {
return o1.resourceId.compareTo(o2.resourceId);
}
};
protected static class RecentUserOrdersDataTable extends RecentOrdersDataTable {
public RecentUserOrdersDataTable() {
super(Models.currentAdminTenant(), NumberUtils.toInt(params.get("offsetInMinutes"), 0));
alterColumn("submittedBy").setVisible(true);
}
}
}