SEBSERV-163 finished grouping with filter

This commit is contained in:
anhefti 2022-09-13 09:36:01 +02:00
parent ee0034c367
commit b5704dea95
18 changed files with 459 additions and 142 deletions

View file

@ -197,6 +197,7 @@ public final class API {
public static final String EXAM_MONITORING_NOTIFICATION_ENDPOINT = "/notification";
public static final String EXAM_MONITORING_DISABLE_CONNECTION_ENDPOINT = "/disable-connection";
public static final String EXAM_MONITORING_STATE_FILTER = "hidden-states";
public static final String EXAM_MONITORING_CLIENT_GROUP_FILTER = "hidden-client-group";
public static final String EXAM_MONITORING_FINISHED_ENDPOINT = "/finishedexams";
public static final String EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT =
"/{" + EXAM_API_SEB_CONNECTION_TOKEN + "}";

View file

@ -28,7 +28,7 @@ import ch.ethz.seb.sebserver.gbl.model.Domain.CLIENT_GROUP;
import ch.ethz.seb.sebserver.gbl.util.Utils;
@JsonIgnoreProperties(ignoreUnknown = true)
public class ClientGroup implements ClientGroupData {
public class ClientGroup implements ClientGroupData, Comparable<ClientGroup> {
public static final String FILTER_ATTR_EXAM_ID = "examId";
@ -285,4 +285,9 @@ public class ClientGroup implements ClientGroupData {
return true;
}
@Override
public int compareTo(final ClientGroup o) {
return o == null ? -1 : this.id.compareTo(o.id);
}
}

View file

@ -84,6 +84,29 @@ public class ClientConnectionData implements GrantEntity {
return this.groups != null && this.groups.contains(clientGroupId);
}
@JsonIgnore
public boolean containsAllClientGroup(final Set<Long> clientGroupIds) {
if (this.groups == null || clientGroupIds == null || clientGroupIds.isEmpty()) {
return false;
}
return this.groups != null && this.groups.containsAll(clientGroupIds);
}
@JsonIgnore
public boolean filter(final Set<Long> clientGroupIdsToHide) {
if (this.groups == null || clientGroupIdsToHide == null || clientGroupIdsToHide.isEmpty()) {
return true;
}
for (final Long id : this.groups) {
if (!clientGroupIdsToHide.contains(id)) {
return true;
}
}
return false;
}
@Override
public EntityType entityType() {
return this.clientConnection.entityType();

View file

@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.gbl.monitoring;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
@ -25,6 +26,7 @@ public class MonitoringSEBConnectionData {
public static final String ATTR_CONNECTIONS = "connections";
public static final String ATTR_STATUS_MAPPING = "statusMapping";
public static final String ATTR_CLIENT_GROUP_MAPPING = "clientGroupMapping";
@JsonProperty(Domain.CLIENT_CONNECTION.ATTR_EXAM_ID)
public final Long examId;
@ -32,16 +34,20 @@ public class MonitoringSEBConnectionData {
public final Collection<ClientConnectionData> connections;
@JsonProperty(ATTR_STATUS_MAPPING)
public final int[] connectionsPerStatus;
@JsonProperty(ATTR_CLIENT_GROUP_MAPPING)
public final Map<Long, Integer> connectionsPerClientGroup;
@JsonCreator
public MonitoringSEBConnectionData(
@JsonProperty(Domain.CLIENT_CONNECTION.ATTR_EXAM_ID) final Long examId,
@JsonProperty(ATTR_CONNECTIONS) final Collection<ClientConnectionData> connections,
@JsonProperty(ATTR_STATUS_MAPPING) final int[] connectionsPerStatus) {
@JsonProperty(ATTR_STATUS_MAPPING) final int[] connectionsPerStatus,
@JsonProperty(ATTR_CLIENT_GROUP_MAPPING) final Map<Long, Integer> connectionsPerClientGroup) {
this.examId = examId;
this.connections = connections;
this.connectionsPerStatus = connectionsPerStatus;
this.connectionsPerClientGroup = connectionsPerClientGroup;
}
public Long getExamId() {
@ -64,6 +70,14 @@ public class MonitoringSEBConnectionData {
return this.connectionsPerStatus[status.code];
}
@JsonIgnore
public int getNumberOfConnection(final Long clientGroupId) {
if (this.connectionsPerClientGroup == null || !this.connectionsPerClientGroup.containsKey(clientGroupId)) {
return -1;
}
return this.connectionsPerClientGroup.get(clientGroupId);
}
@Override
public int hashCode() {
final int prime = 31;

View file

@ -42,7 +42,8 @@ public enum ActionCategory {
LOGS_USER_ACTIVITY_LIST(new LocTextKey("sebserver.userlogs.list.actions"), 1),
LOGS_SEB_CLIENT_LIST(new LocTextKey("sebserver.userlogs.list.actions"), 1),
VARIA(new LocTextKey("sebserver.overall.action.category.varia"), 0),
FILTER(new LocTextKey("sebserver.exam.monitoring.action.category.filter"), 50),
STATE_FILTER(new LocTextKey("sebserver.exam.monitoring.action.category.statefilter"), 40),
GROUP_FILTER(new LocTextKey("sebserver.exam.monitoring.action.category.groupfilter"), 50),
PROCTORING(new LocTextKey("sebserver.exam.overall.action.category.proctoring"), 60),
FINISHED_EXAM_LIST(new LocTextKey("sebserver.finished.exam.list.actions"), 1);

View file

@ -882,44 +882,54 @@ public enum ActionDefinition {
new LocTextKey("sebserver.monitoring.exam.connection.action.hide.requested"),
ImageIcon.TOGGLE_OFF,
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
ActionCategory.FILTER),
ActionCategory.STATE_FILTER),
MONITOR_EXAM_SHOW_REQUESTED_CONNECTION(
new LocTextKey("sebserver.monitoring.exam.connection.action.show.requested"),
ImageIcon.TOGGLE_ON,
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
ActionCategory.FILTER),
ActionCategory.STATE_FILTER),
MONITOR_EXAM_HIDE_ACTIVE_CONNECTION(
new LocTextKey("sebserver.monitoring.exam.connection.action.hide.active"),
ImageIcon.TOGGLE_OFF,
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
ActionCategory.FILTER),
ActionCategory.STATE_FILTER),
MONITOR_EXAM_SHOW_ACTIVE_CONNECTION(
new LocTextKey("sebserver.monitoring.exam.connection.action.show.active"),
ImageIcon.TOGGLE_ON,
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
ActionCategory.FILTER),
ActionCategory.STATE_FILTER),
MONITOR_EXAM_HIDE_CLOSED_CONNECTION(
new LocTextKey("sebserver.monitoring.exam.connection.action.hide.closed"),
ImageIcon.TOGGLE_OFF,
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
ActionCategory.FILTER),
ActionCategory.STATE_FILTER),
MONITOR_EXAM_SHOW_CLOSED_CONNECTION(
new LocTextKey("sebserver.monitoring.exam.connection.action.show.closed"),
ImageIcon.TOGGLE_ON,
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
ActionCategory.FILTER),
ActionCategory.STATE_FILTER),
MONITOR_EXAM_HIDE_DISABLED_CONNECTION(
new LocTextKey("sebserver.monitoring.exam.connection.action.hide.disabled"),
ImageIcon.TOGGLE_OFF,
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
ActionCategory.FILTER),
ActionCategory.STATE_FILTER),
MONITOR_EXAM_SHOW_DISABLED_CONNECTION(
new LocTextKey("sebserver.monitoring.exam.connection.action.show.disabled"),
ImageIcon.TOGGLE_ON,
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
ActionCategory.FILTER),
ActionCategory.STATE_FILTER),
MONITOR_EXAM_HIDE_CLIENT_GROUP_CONNECTION(
new LocTextKey("sebserver.monitoring.exam.connection.action.hide.clientgroup"),
ImageIcon.TOGGLE_OFF,
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
ActionCategory.GROUP_FILTER),
MONITOR_EXAM_SHOW_CLIENT_GROUP_CONNECTION(
new LocTextKey("sebserver.monitoring.exam.connection.action.show.clientgroup"),
ImageIcon.TOGGLE_ON,
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
ActionCategory.GROUP_FILTER),
MONITORING_EXAM_SEARCH_CONNECTIONS(
new LocTextKey("sebserver.monitoring.search.action"),

View file

@ -247,26 +247,7 @@ public class ActionPane implements TemplateComposer {
actionsTitle.setData(RWT.CUSTOM_VARIANT, "close");
actionsTitle.setImage(WidgetFactory.ImageIcon.ACTIVE.getImage(parent.getDisplay()));
actionsTitle.setText("&nbsp;&nbsp;&nbsp;&nbsp;" + titleText);
actionsTitle.addListener(SWT.MouseUp, event -> {
try {
final Control contentControl = composite.getChildren()[1];
if (contentControl.isVisible()) {
actionsTitle.setData(RWT.CUSTOM_VARIANT, "open");
contentControl.setVisible(false);
final GridData l = (GridData) contentControl.getLayoutData();
l.heightHint = 0;
composite.getParent().layout(true, true);
} else {
actionsTitle.setData(RWT.CUSTOM_VARIANT, "close");
contentControl.setVisible(true);
final GridData l = (GridData) contentControl.getLayoutData();
l.heightHint = SWT.DEFAULT;
composite.getParent().layout(true, true);
}
} catch (final Exception e) {
// just ignore
}
});
actionsTitle.addListener(SWT.MouseUp, event -> actionGroupExpand(composite, actionsTitle));
}
actionsTitle.setLayoutData(titleLayout);
@ -319,6 +300,27 @@ public class ActionPane implements TemplateComposer {
return actions;
}
private void actionGroupExpand(final Composite composite, final Label actionsTitle) {
try {
final Control contentControl = composite.getChildren()[1];
if (contentControl.isVisible()) {
actionsTitle.setData(RWT.CUSTOM_VARIANT, "open");
contentControl.setVisible(false);
final GridData l = (GridData) contentControl.getLayoutData();
l.heightHint = 0;
composite.getParent().layout(true, true);
} else {
actionsTitle.setData(RWT.CUSTOM_VARIANT, "close");
contentControl.setVisible(true);
final GridData l = (GridData) contentControl.getLayoutData();
l.heightHint = SWT.DEFAULT;
composite.getParent().layout(true, true);
}
} catch (final Exception e) {
// just ignore
}
}
private void clearDisposedTrees(final Map<String, Tree> actionTrees) {
new ArrayList<>(actionTrees.entrySet())
.forEach(entry -> {

View file

@ -11,6 +11,8 @@ package ch.ethz.seb.sebserver.gui.content.monitoring;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
@ -64,7 +66,7 @@ import ch.ethz.seb.sebserver.gui.service.session.ClientConnectionTable;
import ch.ethz.seb.sebserver.gui.service.session.FullPageMonitoringGUIUpdate;
import ch.ethz.seb.sebserver.gui.service.session.FullPageMonitoringUpdate;
import ch.ethz.seb.sebserver.gui.service.session.InstructionProcessor;
import ch.ethz.seb.sebserver.gui.service.session.MonitoringStatus;
import ch.ethz.seb.sebserver.gui.service.session.MonitoringFilter;
import ch.ethz.seb.sebserver.gui.service.session.proctoring.MonitoringProctoringService;
import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringGUIService;
@ -258,6 +260,7 @@ public class MonitoringRunningExam implements TemplateComposer {
if (isExamSupporter.getAsBoolean()) {
guiUpdates.add(createFilterActions(
clientGroups,
fullPageMonitoringUpdate,
actionBuilder,
clientTable,
@ -347,13 +350,14 @@ public class MonitoringRunningExam implements TemplateComposer {
}
private FullPageMonitoringGUIUpdate createFilterActions(
final MonitoringStatus monitoringStatus,
final Collection<ClientGroup> clientGroups,
final MonitoringFilter monitoringStatus,
final PageActionBuilder actionBuilder,
final ClientConnectionTable clientTable,
final BooleanSupplier isExamSupporter) {
final StatusFilterGUIUpdate statusFilterGUIUpdate =
new StatusFilterGUIUpdate(this.pageService.getPolyglotPageService());
final FilterGUIUpdate statusFilterGUIUpdate =
new FilterGUIUpdate(this.pageService.getPolyglotPageService());
addFilterAction(
monitoringStatus,
@ -388,61 +392,118 @@ public class MonitoringRunningExam implements TemplateComposer {
ActionDefinition.MONITOR_EXAM_SHOW_DISABLED_CONNECTION,
ActionDefinition.MONITOR_EXAM_HIDE_DISABLED_CONNECTION);
if (clientGroups != null && !clientGroups.isEmpty()) {
clientGroups.forEach(clientGroup -> {
addClientGroupFilterAction(
monitoringStatus,
statusFilterGUIUpdate,
actionBuilder,
clientTable,
clientGroup,
ActionDefinition.MONITOR_EXAM_SHOW_CLIENT_GROUP_CONNECTION,
ActionDefinition.MONITOR_EXAM_HIDE_CLIENT_GROUP_CONNECTION);
});
}
return statusFilterGUIUpdate;
}
private void addFilterAction(
final MonitoringStatus monitoringStatus,
final StatusFilterGUIUpdate statusFilterGUIUpdate,
final MonitoringFilter filter,
final FilterGUIUpdate filterGUIUpdate,
final PageActionBuilder actionBuilder,
final ClientConnectionTable clientTable,
final ConnectionStatus status,
final ActionDefinition showActionDef,
final ActionDefinition hideActionDef) {
final int numOfConnections = monitoringStatus.getNumOfConnections(status);
if (monitoringStatus.isStatusHidden(status)) {
final PageAction showAction = actionBuilder.newAction(showActionDef)
.withExec(showStateViewAction(monitoringStatus, clientTable, status))
final int numOfConnections = filter.getNumOfConnections(status);
PageAction action;
if (filter.isStatusHidden(status)) {
action = actionBuilder.newAction(showActionDef)
.withExec(showStateViewAction(filter, clientTable, status))
.noEventPropagation()
.withSwitchAction(
actionBuilder.newAction(hideActionDef)
.withExec(
hideStateViewAction(monitoringStatus, clientTable, status))
hideStateViewAction(filter, clientTable, status))
.noEventPropagation()
.withNameAttributes(numOfConnections)
.create())
.withNameAttributes(numOfConnections)
.create();
this.pageService.publishAction(
showAction,
treeItem -> statusFilterGUIUpdate.register(treeItem, status));
} else {
final PageAction hideAction = actionBuilder.newAction(hideActionDef)
.withExec(hideStateViewAction(monitoringStatus, clientTable, status))
action = actionBuilder.newAction(hideActionDef)
.withExec(hideStateViewAction(filter, clientTable, status))
.noEventPropagation()
.withSwitchAction(
actionBuilder.newAction(showActionDef)
.withExec(
showStateViewAction(monitoringStatus, clientTable, status))
showStateViewAction(filter, clientTable, status))
.noEventPropagation()
.withNameAttributes(numOfConnections)
.create())
.withNameAttributes(numOfConnections)
.create();
this.pageService.publishAction(
hideAction,
treeItem -> statusFilterGUIUpdate.register(treeItem, status));
}
this.pageService.publishAction(
action,
treeItem -> filterGUIUpdate.register(treeItem, status));
}
private void addClientGroupFilterAction(
final MonitoringFilter filter,
final FilterGUIUpdate filterGUIUpdate,
final PageActionBuilder actionBuilder,
final ClientConnectionTable clientTable,
final ClientGroup clientGroup,
final ActionDefinition showActionDef,
final ActionDefinition hideActionDef) {
final int numOfConnections = filter.getNumOfConnections(clientGroup.id);
PageAction action;
if (filter.isClientGroupHidden(clientGroup.id)) {
action = actionBuilder.newAction(showActionDef)
.withExec(showClientGroupAction(filter, clientTable, clientGroup.id))
.noEventPropagation()
.withSwitchAction(
actionBuilder.newAction(hideActionDef)
.withExec(
hideClientGroupViewAction(filter, clientTable, clientGroup.id))
.noEventPropagation()
.withNameAttributes(clientGroup.name, numOfConnections)
.create())
.withNameAttributes(clientGroup.name, numOfConnections)
.create();
} else {
action = actionBuilder.newAction(hideActionDef)
.withExec(hideClientGroupViewAction(filter, clientTable, clientGroup.id))
.noEventPropagation()
.withSwitchAction(
actionBuilder.newAction(showActionDef)
.withExec(
showClientGroupAction(filter, clientTable, clientGroup.id))
.noEventPropagation()
.withNameAttributes(clientGroup.name, numOfConnections)
.create())
.withNameAttributes(clientGroup.name, numOfConnections)
.create();
}
this.pageService.publishAction(
action,
treeItem -> filterGUIUpdate.register(treeItem, clientGroup.id));
}
/** This holds the filter action items and implements the specific GUI update for it */
private class StatusFilterGUIUpdate implements FullPageMonitoringGUIUpdate {
private class FilterGUIUpdate implements FullPageMonitoringGUIUpdate {
private final PolyglotPageService polyglotPageService;
private final TreeItem[] actionItemPerStateFilter = new TreeItem[ConnectionStatus.values().length];
private final Map<Long, TreeItem> actionItemPerClientGroup = new HashMap<>();
public StatusFilterGUIUpdate(final PolyglotPageService polyglotPageService) {
public FilterGUIUpdate(final PolyglotPageService polyglotPageService) {
this.polyglotPageService = polyglotPageService;
}
@ -450,8 +511,12 @@ public class MonitoringRunningExam implements TemplateComposer {
this.actionItemPerStateFilter[status.code] = item;
}
void register(final TreeItem item, final Long clientGroupId) {
this.actionItemPerClientGroup.put(clientGroupId, item);
}
@Override
public void update(final MonitoringStatus monitoringStatus) {
public void update(final MonitoringFilter monitoringStatus) {
final ConnectionStatus[] states = ConnectionStatus.values();
for (int i = 0; i < states.length; i++) {
final ConnectionStatus state = states[i];
@ -463,6 +528,18 @@ public class MonitoringRunningExam implements TemplateComposer {
this.polyglotPageService.injectI18n(treeItem, action.getTitle());
}
}
if (!this.actionItemPerClientGroup.isEmpty()) {
this.actionItemPerClientGroup.entrySet().stream().forEach(entry -> {
final int numOfConnections = monitoringStatus.getNumOfConnections(entry.getKey());
if (numOfConnections >= 0) {
final TreeItem treeItem = entry.getValue();
final PageAction action = (PageAction) treeItem.getData(ActionPane.ACTION_EVENT_CALL_KEY);
action.setTitleArgument(1, numOfConnections);
this.polyglotPageService.injectI18n(treeItem, action.getTitle());
}
});
}
}
}
@ -472,7 +549,7 @@ public class MonitoringRunningExam implements TemplateComposer {
}
private static Function<PageAction, PageAction> showStateViewAction(
final MonitoringStatus monitoringStatus,
final MonitoringFilter monitoringStatus,
final ClientConnectionTable clientTable,
final ConnectionStatus status) {
@ -484,7 +561,7 @@ public class MonitoringRunningExam implements TemplateComposer {
}
private static Function<PageAction, PageAction> hideStateViewAction(
final MonitoringStatus monitoringStatus,
final MonitoringFilter monitoringStatus,
final ClientConnectionTable clientTable,
final ConnectionStatus status) {
@ -495,6 +572,30 @@ public class MonitoringRunningExam implements TemplateComposer {
};
}
private static Function<PageAction, PageAction> showClientGroupAction(
final MonitoringFilter monitoringStatus,
final ClientConnectionTable clientTable,
final Long clientGroupId) {
return action -> {
monitoringStatus.showClientGroup(clientGroupId);
clientTable.removeSelection();
return action;
};
}
private static Function<PageAction, PageAction> hideClientGroupViewAction(
final MonitoringFilter monitoringStatus,
final ClientConnectionTable clientTable,
final Long clientGroupId) {
return action -> {
monitoringStatus.hideClientGroup(clientGroupId);
clientTable.removeSelection();
return action;
};
}
private Set<EntityKey> selectionForInstruction(final ClientConnectionTable clientTable) {
final Set<String> connectionTokens = clientTable.getConnectionTokens(
cc -> cc.status.clientActiveStatus,

View file

@ -312,10 +312,10 @@ public final class ClientConnectionTable implements FullPageMonitoringGUIUpdate
}
@Override
public void update(final MonitoringStatus monitoringStatus) {
public void update(final MonitoringFilter monitoringStatus) {
final Collection<ClientConnectionData> connectionData = monitoringStatus.getConnectionData();
final boolean sizeChanged = connectionData.size() != this.table.getItemCount();
final boolean needsSync = monitoringStatus.statusFilterChanged() ||
final boolean needsSync = monitoringStatus.filterChanged() ||
this.forceUpdateAll ||
sizeChanged ||
(this.tableMapping != null &&
@ -350,7 +350,7 @@ public final class ClientConnectionTable implements FullPageMonitoringGUIUpdate
}
}
});
monitoringStatus.resetStatusFilterChanged();
monitoringStatus.resetFilterChanged();
this.toDelete.clear();
}
@ -624,7 +624,7 @@ public final class ClientConnectionTable implements FullPageMonitoringGUIUpdate
private String getGroupInfo() {
final StringBuilder sb = new StringBuilder();
ClientConnectionTable.this.clientGroupMapping.keySet().stream().forEach(key -> {
if (this.connectionData.groups.contains(key)) {
if (this.connectionData.groups != null && this.connectionData.groups.contains(key)) {
final ClientGroup clientGroup = ClientConnectionTable.this.clientGroupMapping.get(key);
sb.append(WidgetFactory.getTextWithBackgroundHTML(clientGroup.name, clientGroup.color));
}

View file

@ -11,6 +11,6 @@ package ch.ethz.seb.sebserver.gui.service.session;
@FunctionalInterface
public interface FullPageMonitoringGUIUpdate {
void update(MonitoringStatus monitoringStatus);
void update(MonitoringFilter monitoringStatus);
}

View file

@ -10,7 +10,10 @@ package ch.ethz.seb.sebserver.gui.service.session;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.swt.widgets.Composite;
@ -20,6 +23,8 @@ import org.slf4j.LoggerFactory;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.async.AsyncRunner;
import ch.ethz.seb.sebserver.gbl.model.exam.ClientGroup;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.monitoring.MonitoringFullPageData;
import ch.ethz.seb.sebserver.gbl.util.Utils;
@ -29,6 +34,7 @@ import ch.ethz.seb.sebserver.gui.service.push.ServerPushContext;
import ch.ethz.seb.sebserver.gui.service.push.ServerPushService;
import ch.ethz.seb.sebserver.gui.service.push.UpdateErrorHandler;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.clientgroup.GetClientGroups;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetMonitoringFullPageData;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.DisposedOAuth2RestTemplateException;
@ -36,11 +42,12 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.DisposedOAuth2Re
* full page monitoring.
*
* This handles server push and GUI update and also implements kind of circuit breaker and error handling */
public class FullPageMonitoringUpdate implements MonitoringStatus {
public class FullPageMonitoringUpdate implements MonitoringFilter {
static final Logger log = LoggerFactory.getLogger(FullPageMonitoringUpdate.class);
private static final String USER_SESSION_STATUS_FILTER_ATTRIBUTE = "USER_SESSION_STATUS_FILTER_ATTRIBUTE";
private static final String USER_SESSION_STATUS_FILTER_ATTRIBUTE = "USER_SESSION_STATUS_FILTER";
private static final String USER_SESSION_GROUP_FILTER_ATTRIBUTE = "USER_SESSION_GROUP_FILTER";
private final ServerPushService serverPushService;
private final PageService pageService;
@ -49,9 +56,13 @@ public class FullPageMonitoringUpdate implements MonitoringStatus {
private final Collection<FullPageMonitoringGUIUpdate> guiUpdates;
private ServerPushContext pushContext;
private final EnumSet<ConnectionStatus> statusFilter;
private String statusFilterParam = "";
private boolean statusFilterChanged = false;
private final Set<Long> clientGroupFilter;
private String clientGroupFilterParam = "";
private boolean filterChanged = false;
private boolean updateInProgress = false;
private MonitoringFullPageData monitoringFullPageData = null;
@ -72,7 +83,19 @@ public class FullPageMonitoringUpdate implements MonitoringStatus {
this.guiUpdates = guiUpdates;
this.statusFilter = EnumSet.noneOf(ConnectionStatus.class);
loadStatusFilter();
loadFilter();
final Collection<ClientGroup> clientGroups = pageService.getRestService()
.getBuilder(GetClientGroups.class)
.withQueryParam(Indicator.FILTER_ATTR_EXAM_ID, String.valueOf(examId))
.call()
.getOr(Collections.emptyList());
if (clientGroups != null && !clientGroups.isEmpty()) {
this.clientGroupFilter = new HashSet<>();
} else {
this.clientGroupFilter = null;
}
}
public void start(final PageContext pageContext, final Composite anchor, final long pollInterval) {
@ -105,13 +128,13 @@ public class FullPageMonitoringUpdate implements MonitoringStatus {
}
@Override
public boolean statusFilterChanged() {
return this.statusFilterChanged;
public boolean filterChanged() {
return this.filterChanged;
}
@Override
public void resetStatusFilterChanged() {
this.statusFilterChanged = false;
public void resetFilterChanged() {
this.filterChanged = false;
}
@Override
@ -122,13 +145,43 @@ public class FullPageMonitoringUpdate implements MonitoringStatus {
@Override
public void hideStatus(final ConnectionStatus status) {
this.statusFilter.add(status);
saveStatusFilter();
saveFilter();
}
@Override
public void showStatus(final ConnectionStatus status) {
this.statusFilter.remove(status);
saveStatusFilter();
saveFilter();
}
@Override
public boolean hasClientGroupFilter() {
return this.clientGroupFilter != null;
}
@Override
public boolean isClientGroupHidden(final Long clientGroupId) {
return this.clientGroupFilter != null && this.clientGroupFilter.contains(clientGroupId);
}
@Override
public void hideClientGroup(final Long clientGroupId) {
if (this.clientGroupFilter == null) {
return;
}
this.clientGroupFilter.add(clientGroupId);
saveFilter();
}
@Override
public void showClientGroup(final Long clientGroupId) {
if (this.clientGroupFilter == null) {
return;
}
this.clientGroupFilter.remove(clientGroupId);
saveFilter();
}
@Override
@ -161,8 +214,15 @@ public class FullPageMonitoringUpdate implements MonitoringStatus {
}
private void updateBusinessData() {
this.monitoringFullPageData = this.restCallBuilder
.withHeader(API.EXAM_MONITORING_STATE_FILTER, this.statusFilterParam)
RestCall<MonitoringFullPageData>.RestCallBuilder restCallBuilder = this.restCallBuilder
.withHeader(API.EXAM_MONITORING_STATE_FILTER, this.statusFilterParam);
if (hasClientGroupFilter()) {
restCallBuilder = restCallBuilder
.withHeader(API.EXAM_MONITORING_CLIENT_GROUP_FILTER, this.clientGroupFilterParam);
}
this.monitoringFullPageData = restCallBuilder
.call()
.get(error -> {
recoverFromDisposedRestTemplate(error);
@ -182,22 +242,32 @@ public class FullPageMonitoringUpdate implements MonitoringStatus {
});
}
private void saveStatusFilter() {
private void saveFilter() {
try {
this.pageService
.getCurrentUser()
.putAttribute(
USER_SESSION_STATUS_FILTER_ATTRIBUTE,
StringUtils.join(this.statusFilter, Constants.LIST_SEPARATOR));
if (hasClientGroupFilter()) {
this.pageService
.getCurrentUser()
.putAttribute(
USER_SESSION_GROUP_FILTER_ATTRIBUTE,
StringUtils.join(this.clientGroupFilter, Constants.LIST_SEPARATOR));
}
} catch (final Exception e) {
log.warn("Failed to save status filter to user session");
} finally {
this.statusFilterParam = StringUtils.join(this.statusFilter, Constants.LIST_SEPARATOR);
this.statusFilterChanged = true;
if (hasClientGroupFilter()) {
this.clientGroupFilterParam = StringUtils.join(this.clientGroupFilter, Constants.LIST_SEPARATOR);
}
this.filterChanged = true;
}
}
private void loadStatusFilter() {
private void loadFilter() {
try {
final String attribute = this.pageService
.getCurrentUser()
@ -206,17 +276,33 @@ public class FullPageMonitoringUpdate implements MonitoringStatus {
if (attribute != null) {
Arrays.asList(StringUtils.split(attribute, Constants.LIST_SEPARATOR))
.forEach(name -> this.statusFilter.add(ConnectionStatus.valueOf(name)));
} else {
this.statusFilter.add(ConnectionStatus.DISABLED);
}
if (hasClientGroupFilter()) {
final String groups = this.pageService
.getCurrentUser()
.getAttribute(USER_SESSION_GROUP_FILTER_ATTRIBUTE);
this.statusFilter.clear();
if (groups != null) {
Arrays.asList(StringUtils.split(groups, Constants.LIST_SEPARATOR))
.forEach(id -> this.clientGroupFilter.add(Long.parseLong(id)));
}
}
} catch (final Exception e) {
log.warn("Failed to load status filter to user session");
this.statusFilter.clear();
this.statusFilter.add(ConnectionStatus.DISABLED);
if (hasClientGroupFilter()) {
this.clientGroupFilter.clear();
}
} finally {
this.statusFilterParam = StringUtils.join(this.statusFilter, Constants.LIST_SEPARATOR);
this.statusFilterChanged = true;
if (hasClientGroupFilter()) {
this.clientGroupFilterParam = StringUtils.join(this.clientGroupFilter, Constants.LIST_SEPARATOR);
}
this.filterChanged = true;
}
}

View file

@ -12,21 +12,22 @@ import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import ch.ethz.seb.sebserver.gbl.model.exam.ClientGroup;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.monitoring.MonitoringFullPageData;
import ch.ethz.seb.sebserver.gbl.monitoring.MonitoringSEBConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom;
import ch.ethz.seb.sebserver.gbl.monitoring.MonitoringFullPageData;
import ch.ethz.seb.sebserver.gbl.monitoring.MonitoringSEBConnectionData;
public interface MonitoringStatus {
public interface MonitoringFilter {
EnumSet<ConnectionStatus> getStatusFilter();
String getStatusFilterParam();
boolean statusFilterChanged();
boolean filterChanged();
void resetStatusFilterChanged();
void resetFilterChanged();
boolean isStatusHidden(ConnectionStatus status);
@ -34,6 +35,18 @@ public interface MonitoringStatus {
void showStatus(ConnectionStatus status);
boolean hasClientGroupFilter();
default boolean isClientGroupHidden(final ClientGroup clientGroup) {
return isClientGroupHidden(clientGroup.id);
}
boolean isClientGroupHidden(Long clientGroupId);
void hideClientGroup(Long clientGroupId);
void showClientGroup(Long clientGroupId);
MonitoringFullPageData getMonitoringFullPageData();
default MonitoringSEBConnectionData getMonitoringSEBConnectionData() {
@ -63,6 +76,15 @@ public interface MonitoringStatus {
}
}
default int getNumOfConnections(final Long clientGroupId) {
final MonitoringSEBConnectionData monitoringSEBConnectionData = getMonitoringSEBConnectionData();
if (monitoringSEBConnectionData != null) {
return monitoringSEBConnectionData.getNumberOfConnection(clientGroupId);
} else {
return 0;
}
}
default Collection<RemoteProctoringRoom> proctoringData() {
final MonitoringFullPageData monitoringFullPageData = getMonitoringFullPageData();
if (monitoringFullPageData != null) {

View file

@ -194,6 +194,7 @@ public class ClientGroupDAOImpl implements ClientGroupDAO {
.stream()
.map(this::toDomainModel)
.flatMap(DAOLoggingSupport::logAndSkipOnError)
.sorted()
.collect(Collectors.toList()));
}

View file

@ -39,6 +39,8 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ExamConfigService
@WebServiceProfile
public class ExamSessionCacheService {
public static final Object CLIENT_CONECTION_CREATION_LOCK = new Object();
public static final String CACHE_NAME_RUNNING_EXAM = "RUNNING_EXAM";
public static final String CACHE_NAME_ACTIVE_CLIENT_CONNECTION = "ACTIVE_CLIENT_CONNECTION";
public static final String CACHE_NAME_SEB_CONFIG_EXAM = "SEB_CONFIG_EXAM";

View file

@ -13,7 +13,9 @@ import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
@ -34,6 +36,7 @@ import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.ErrorMessage;
import ch.ethz.seb.sebserver.gbl.model.Entity;
import ch.ethz.seb.sebserver.gbl.model.exam.ClientGroup;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
@ -43,6 +46,7 @@ import ch.ethz.seb.sebserver.gbl.monitoring.MonitoringSEBConnectionData;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientGroupDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
@ -64,6 +68,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
private final IndicatorDAO indicatorDAO;
private final ExamSessionCacheService examSessionCacheService;
private final ExamDAO examDAO;
private final ClientGroupDAO clientGroupDAO;
private final ExamConfigurationMapDAO examConfigurationMapDAO;
private final CacheManager cacheManager;
private final SEBRestrictionService sebRestrictionService;
@ -76,6 +81,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
protected ExamSessionServiceImpl(
final ExamSessionCacheService examSessionCacheService,
final ExamDAO examDAO,
final ClientGroupDAO clientGroupDAO,
final ExamConfigurationMapDAO examConfigurationMapDAO,
final ClientConnectionDAO clientConnectionDAO,
final IndicatorDAO indicatorDAO,
@ -87,6 +93,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
this.examSessionCacheService = examSessionCacheService;
this.examDAO = examDAO;
this.clientGroupDAO = clientGroupDAO;
this.examConfigurationMapDAO = examConfigurationMapDAO;
this.clientConnectionDAO = clientConnectionDAO;
this.cacheManager = cacheManager;
@ -350,7 +357,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
@Override
public ClientConnectionDataInternal getConnectionDataInternal(final String connectionToken) {
synchronized (this.examSessionCacheService) {
synchronized (ExamSessionCacheService.CLIENT_CONECTION_CREATION_LOCK) {
return this.examSessionCacheService.getClientConnection(connectionToken);
}
}
@ -405,25 +412,33 @@ public class ExamSessionServiceImpl implements ExamSessionService {
for (int i = 0; i < statusMapping.length; i++) {
statusMapping[i] = 0;
}
// needed to store connection numbers per client group too
final Collection<ClientGroup> groups = this.clientGroupDAO.allForExam(examId).getOr(null);
final Map<Long, Integer> clientGroupMapping = (groups != null && !groups.isEmpty())
? new HashMap<>()
: null;
updateClientConnections(examId);
synchronized (this.examSessionCacheService) {
final List<ClientConnectionData> filteredConnections = this.clientConnectionDAO
.getConnectionTokens(examId)
.getOrThrow()
.stream()
.map(token -> this.examSessionCacheService.getClientConnection(token))
.filter(Objects::nonNull)
.map(c -> {
statusMapping[c.clientConnection.status.code]++;
return c;
})
.filter(filter)
.collect(Collectors.toList());
final List<ClientConnectionData> filteredConnections = this.clientConnectionDAO
.getConnectionTokens(examId)
.getOrThrow()
.stream()
.map(token -> getConnectionDataInternal(token))
.filter(Objects::nonNull)
.map(c -> {
statusMapping[c.clientConnection.status.code]++;
processClientGroupMapping(c.groups, clientGroupMapping);
return c;
})
.filter(filter)
.collect(Collectors.toList());
return new MonitoringSEBConnectionData(examId, filteredConnections, statusMapping);
}
return new MonitoringSEBConnectionData(
examId,
filteredConnections,
statusMapping,
clientGroupMapping);
});
}
@ -557,4 +572,18 @@ public class ExamSessionServiceImpl implements ExamSessionService {
&& Objects.equals(exam.name, runningExam.name);
}
private void processClientGroupMapping(final Set<Long> groups, final Map<Long, Integer> clientGroupMapping) {
if (groups == null || clientGroupMapping == null) {
return;
}
groups.forEach(id -> {
if (clientGroupMapping.containsKey(id)) {
clientGroupMapping.put(id, clientGroupMapping.get(id) + 1);
} else {
clientGroupMapping.put(id, 1);
}
});
}
}

View file

@ -692,8 +692,10 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
private void processPing(final String connectionToken, final long timestamp, final int pingNumber) {
final ClientConnectionDataInternal activeClientConnection =
this.examSessionCacheService.getClientConnection(connectionToken);
ClientConnectionDataInternal activeClientConnection = null;
synchronized (ExamSessionCacheService.CLIENT_CONECTION_CREATION_LOCK) {
activeClientConnection = this.examSessionCacheService.getClientConnection(connectionToken);
}
if (activeClientConnection != null) {
activeClientConnection.notifyPing(timestamp, pingNumber);

View file

@ -11,7 +11,9 @@ package ch.ethz.seb.sebserver.webservice.weblayer.api;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.function.Predicate;
@ -248,31 +250,17 @@ public class ExamMonitoringController {
required = true,
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId,
@PathVariable(name = API.PARAM_PARENT_MODEL_ID, required = true) final Long examId,
@RequestHeader(name = API.EXAM_MONITORING_STATE_FILTER, required = false) final String hiddenStates) {
@RequestHeader(name = API.EXAM_MONITORING_STATE_FILTER, required = false) final String hiddenStates,
@RequestHeader(
name = API.EXAM_MONITORING_CLIENT_GROUP_FILTER,
required = false) final String hiddenClientGroups) {
checkPrivileges(institutionId, examId);
final EnumSet<ConnectionStatus> filterStates = EnumSet.noneOf(ConnectionStatus.class);
if (StringUtils.isNoneBlank(hiddenStates)) {
final String[] split = StringUtils.split(hiddenStates, Constants.LIST_SEPARATOR);
for (int i = 0; i < split.length; i++) {
filterStates.add(ConnectionStatus.valueOf(split[i]));
}
}
final boolean active = filterStates.contains(ConnectionStatus.ACTIVE);
if (active) {
filterStates.remove(ConnectionStatus.ACTIVE);
}
return this.examSessionService
.getMonitoringSEBConnectionsData(
examId,
filterStates.isEmpty()
? Objects::nonNull
: active
? withActiveFilter(filterStates)
: noneActiveFilter(filterStates))
createMonitoringFilter(hiddenStates, hiddenClientGroups))
.getOrThrow().connections;
}
@ -288,31 +276,17 @@ public class ExamMonitoringController {
required = true,
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId,
@PathVariable(name = API.PARAM_PARENT_MODEL_ID, required = true) final Long examId,
@RequestHeader(name = API.EXAM_MONITORING_STATE_FILTER, required = false) final String hiddenStates) {
@RequestHeader(name = API.EXAM_MONITORING_STATE_FILTER, required = false) final String hiddenStates,
@RequestHeader(
name = API.EXAM_MONITORING_CLIENT_GROUP_FILTER,
required = false) final String hiddenClientGroups) {
final Exam runningExam = checkPrivileges(institutionId, examId);
final EnumSet<ConnectionStatus> filterStates = EnumSet.noneOf(ConnectionStatus.class);
if (StringUtils.isNoneBlank(hiddenStates)) {
final String[] split = StringUtils.split(hiddenStates, Constants.LIST_SEPARATOR);
for (int i = 0; i < split.length; i++) {
filterStates.add(ConnectionStatus.valueOf(split[i]));
}
}
final boolean active = filterStates.contains(ConnectionStatus.ACTIVE);
if (active) {
filterStates.remove(ConnectionStatus.ACTIVE);
}
final MonitoringSEBConnectionData monitoringSEBConnectionData = this.examSessionService
.getMonitoringSEBConnectionsData(
examId,
filterStates.isEmpty()
? Objects::nonNull
: active
? withActiveFilter(filterStates)
: noneActiveFilter(filterStates))
createMonitoringFilter(hiddenStates, hiddenClientGroups))
.getOrThrow();
if (this.examAdminService.isProctoringEnabled(runningExam).getOr(false)) {
@ -509,4 +483,46 @@ public class ExamMonitoringController {
};
}
private Predicate<ClientConnectionData> createMonitoringFilter(
final String hiddenStates,
final String hiddenClientGroups) {
final EnumSet<ConnectionStatus> filterStates = EnumSet.noneOf(ConnectionStatus.class);
if (StringUtils.isNotBlank(hiddenStates)) {
final String[] split = StringUtils.split(hiddenStates, Constants.LIST_SEPARATOR);
for (int i = 0; i < split.length; i++) {
filterStates.add(ConnectionStatus.valueOf(split[i]));
}
}
final boolean active = filterStates.contains(ConnectionStatus.ACTIVE);
if (active) {
filterStates.remove(ConnectionStatus.ACTIVE);
}
final Predicate<ClientConnectionData> stateFilter = filterStates.isEmpty()
? Objects::nonNull
: active
? withActiveFilter(filterStates)
: noneActiveFilter(filterStates);
Set<Long> filterClientGroups = null;
if (StringUtils.isNotBlank(hiddenClientGroups)) {
filterClientGroups = new HashSet<>();
final String[] split = StringUtils.split(hiddenClientGroups, Constants.LIST_SEPARATOR);
for (int i = 0; i < split.length; i++) {
filterClientGroups.add(Long.parseLong(split[i]));
}
}
final Set<Long> _filterClientGroups = filterClientGroups;
final Predicate<ClientConnectionData> filter = ccd -> {
if (ccd == null) {
return false;
}
return stateFilter.test(ccd) && ccd.filter(_filterClientGroups);
};
return filter;
}
}

View file

@ -1885,7 +1885,8 @@ sebserver.monitoring.exam.list.actions=
sebserver.monitoring.exam.action.detail.view=Back To Monitoring
sebserver.monitoring.exam.action.list.view=Monitoring
sebserver.monitoring.exam.action.viewroom=View {0} ( {1} / {2} )
sebserver.exam.monitoring.action.category.filter=Filter
sebserver.exam.monitoring.action.category.statefilter=State Filter
sebserver.exam.monitoring.action.category.groupfilter=Client Group Filter
sebserver.exam.overall.action.category.proctoring=Proctoring
sebserver.monitoring.exam.action.proctoring.openTownhall=Open Townhall
sebserver.monitoring.exam.action.proctoring.showTownhall=Show Townhall
@ -1971,6 +1972,8 @@ sebserver.monitoring.exam.connection.action.hide.disabled=Hide Canceled ( {0} )
sebserver.monitoring.exam.connection.action.show.disabled=Show Canceled ( {0} )
sebserver.monitoring.exam.connection.action.hide.undefined=Hide Undefined ( {0} )
sebserver.monitoring.exam.connection.action.show.undefined=Show Undefined ( {0} )
sebserver.monitoring.exam.connection.action.hide.clientgroup=Hide {0} ( {1} )
sebserver.monitoring.exam.connection.action.show.clientgroup=Show {0} ( {1} )
sebserver.monitoring.exam.connection.action.proctoring=Single Room Proctoring
sebserver.monitoring.exam.connection.action.proctoring.examroom=Exam Room Proctoring
sebserver.monitoring.exam.connection.action.openTownhall.confirm=You are about to open the town-hall room and force all SEB clients to join the town-hall room.<br/>Are you sure to open the town-hall?
@ -1978,7 +1981,6 @@ sebserver.monitoring.exam.connection.action.closeTownhall.confirm=You are about
sebserver.monitoring.exam.connection.action.singleroom.confirm=You are about to open the single/one to one room for this participant.<br/>Are you sure you want to open the single room?
sebserver.monitoring.exam.connection.actions.group2=&nbsp;
sebserver.monitoring.exam.connection.actions.group3=&nbsp;
sebserver.monitoring.exam.connection.notificationlist.actions=
sebserver.monitoring.exam.connection.action.confirm.notification=Confirm Notification
sebserver.monitoring.exam.connection.action.confirm.notification.text=Are you sure you want to confirm this pending notification?<br/><br/>Note that this will send a notification confirmation instruction to the SEB client and remove this notification from the pending list.