SEBSERV-147 GUI implementation

This commit is contained in:
anhefti 2020-11-30 13:01:53 +01:00
parent 9267d34d3a
commit a7a4b60917
10 changed files with 352 additions and 20 deletions

View file

@ -13,6 +13,7 @@ import java.util.Base64.Encoder;
import java.util.Collection;
import java.util.Optional;
import org.apache.commons.lang3.BooleanUtils;
import org.eclipse.rap.rwt.RWT;
import org.eclipse.rap.rwt.client.service.JavaScriptExecutor;
import org.eclipse.swt.widgets.Composite;
@ -57,7 +58,9 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetIndicators;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetProctoringSettings;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.logs.GetExtendedClientEventPage;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.ConfirmPendingClientNotification;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetClientConnectionData;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetPendingClientNotifications;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetProcotringRooms;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetProctorRoomConnectionData;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
@ -66,6 +69,7 @@ import ch.ethz.seb.sebserver.gui.service.session.InstructionProcessor;
import ch.ethz.seb.sebserver.gui.service.session.ProctoringGUIService;
import ch.ethz.seb.sebserver.gui.table.ColumnDefinition;
import ch.ethz.seb.sebserver.gui.table.ColumnDefinition.TableFilterAttribute;
import ch.ethz.seb.sebserver.gui.table.EntityTable;
import ch.ethz.seb.sebserver.gui.table.TableFilter.CriteriaType;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
@ -76,19 +80,16 @@ public class MonitoringClientConnection implements TemplateComposer {
private static final Logger log = LoggerFactory.getLogger(MonitoringClientConnection.class);
// // @formatter:off
// private static final String OPEN_SINGEL_ROOM_SCRIPT =
// "var existingWin = window.open('', '%s', 'height=420,width=640,location=no,scrollbars=yes,status=no,menubar=yes,toolbar=yes,titlebar=yes,dialog=yes');\n" +
// "if(existingWin.location.href === 'about:blank'){\n" +
// " existingWin.location.href = '%s%s';\n" +
// " existingWin.focus();\n" +
// "} else {\n" +
// " existingWin.focus();\n" +
// "}";
// // @formatter:on
private static final LocTextKey PAGE_TITLE_KEY =
new LocTextKey("sebserver.monitoring.exam.connection.title");
private static final LocTextKey NOTIFICATION_LIST_TITLE_KEY =
new LocTextKey("sebserver.monitoring.exam.connection.notificationlist.title");
private static final LocTextKey NOTIFICATION_LIST_TITLE_TOOLTIP_KEY =
new LocTextKey("sebserver.monitoring.exam.connection.notificationlist.title.tooltip");
private static final LocTextKey NOTIFICATION_LIST_CONFIRM_TEXT_KEY =
new LocTextKey("monitoring.exam.connection.action.confirm.notification.text");
private static final LocTextKey EVENT_LIST_TITLE_KEY =
new LocTextKey("sebserver.monitoring.exam.connection.eventlist.title");
private static final LocTextKey EVENT_LIST_TITLE_TOOLTIP_KEY =
@ -205,17 +206,77 @@ public class MonitoringClientConnection implements TemplateComposer {
context1 -> clientConnectionDetails.updateData(),
context -> clientConnectionDetails.updateGUI());
widgetFactory.addFormSubContextHeader(
content,
EVENT_LIST_TITLE_KEY,
EVENT_LIST_TITLE_TOOLTIP_KEY);
final PageService.PageActionBuilder actionBuilder = this.pageService
.pageActionBuilder(
pageContext
.clearAttributes()
.clearEntityKeys());
final boolean hasNotification = BooleanUtils.isTrue(connectionData.pendingNotification());
if (hasNotification) {
// add notification table
widgetFactory.addFormSubContextHeader(
content,
NOTIFICATION_LIST_TITLE_KEY,
NOTIFICATION_LIST_TITLE_TOOLTIP_KEY);
final EntityTable<ClientEvent> notificationTable = this.pageService.remoteListTableBuilder(
restService.getRestCall(GetPendingClientNotifications.class),
EntityType.CLIENT_EVENT)
.withRestCallAdapter(builder -> builder.withURIVariable(
API.PARAM_PARENT_MODEL_ID,
parentEntityKey.modelId)
.withURIVariable(
API.EXAM_API_SEB_CONNECTION_TOKEN,
connectionToken))
.withPaging(5)
.withColumn(new ColumnDefinition<ClientEvent>(
Domain.CLIENT_EVENT.ATTR_TYPE,
LIST_COLUMN_TYPE_KEY,
this.resourceService::getEventTypeName)
.sortable()
.widthProportion(2))
.withColumn(new ColumnDefinition<>(
Domain.CLIENT_EVENT.ATTR_TEXT,
LIST_COLUMN_TEXT_KEY,
ClientEvent::getText)
.sortable()
.withCellTooltip()
.widthProportion(4))
.withColumn(new ColumnDefinition<>(
Domain.CLIENT_EVENT.ATTR_SERVER_TIME,
new LocTextKey(LIST_COLUMN_SERVER_TIME_KEY.name,
this.i18nSupport.getUsersTimeZoneTitleSuffix()),
this::getServerTime)
.sortable()
.widthProportion(1))
.withDefaultAction(t -> actionBuilder
.newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION_CONFIRM_NOTIFICATION)
.withParentEntityKey(parentEntityKey)
.withConfirm(() -> NOTIFICATION_LIST_CONFIRM_TEXT_KEY)
.withExec(action -> this.confirmNotification(action, connectionData))
.noEventPropagation()
.create())
.withSelectionListener(this.pageService.getSelectionPublisher(
pageContext,
ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION_CONFIRM_NOTIFICATION))
.compose(pageContext.copyOf(content));
actionBuilder
.newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION_CONFIRM_NOTIFICATION)
.withParentEntityKey(parentEntityKey)
.withConfirm(() -> NOTIFICATION_LIST_CONFIRM_TEXT_KEY)
.withExec(action -> this.confirmNotification(action, connectionData))
.publishIf(() -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER));
}
widgetFactory.addFormSubContextHeader(
content,
EVENT_LIST_TITLE_KEY,
EVENT_LIST_TITLE_TOOLTIP_KEY);
// client event table for this connection
this.pageService.entityTableBuilder(restService.getRestCall(GetExtendedClientEventPage.class))
.withEmptyMessage(EMPTY_LIST_TEXT_KEY)
@ -325,6 +386,24 @@ public class MonitoringClientConnection implements TemplateComposer {
}
}
private PageAction confirmNotification(
final PageAction pageAction,
final ClientConnectionData connectionData) {
final EntityKey entityKey = pageAction.getSingleSelection();
final EntityKey parentEntityKey = pageAction.getParentEntityKey();
this.pageService.getRestService()
.getBuilder(ConfirmPendingClientNotification.class)
.withURIVariable(API.PARAM_PARENT_MODEL_ID, parentEntityKey.modelId)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.withURIVariable(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionData.clientConnection.connectionToken)
.call()
.getOrThrow();
return pageAction;
}
private PageAction openExamCollectionProctorScreen(
final PageAction action,
final ClientConnectionData connectionData) {

View file

@ -640,6 +640,11 @@ public enum ActionDefinition {
ImageIcon.PROCTOR_ROOM,
PageStateDefinitionImpl.MONITORING_CLIENT_CONNECTION,
ActionCategory.FORM),
MONITOR_EXAM_CLIENT_CONNECTION_CONFIRM_NOTIFICATION(
new LocTextKey("sebserver.monitoring.exam.connection.action.confirm.notification"),
ImageIcon.YES,
PageStateDefinitionImpl.MONITORING_CLIENT_CONNECTION,
ActionCategory.FORM),
MONITOR_EXAM_QUIT_SELECTED(
new LocTextKey("sebserver.monitoring.exam.connection.action.instruction.quit.selected"),

View file

@ -9,6 +9,7 @@
package ch.ethz.seb.sebserver.gui.service.page;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@ -336,6 +337,8 @@ public interface PageService {
<T> TableBuilder<T> staticListTableBuilder(final List<T> staticList, EntityType entityType);
<T> TableBuilder<T> remoteListTableBuilder(RestCall<Collection<T>> apiCall, EntityType entityType);
/** Get a new PageActionBuilder for a given PageContext.
*
* @param pageContext the PageContext that is used by the new PageActionBuilder

View file

@ -110,6 +110,10 @@ public final class PageAction {
return this.pageContext.getEntityKey();
}
public EntityKey getParentEntityKey() {
return this.pageContext.getParentEntityKey();
}
public PageContext pageContext() {
return this.pageContext;
}

View file

@ -60,6 +60,7 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.AuthorizationContextHolder;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
import ch.ethz.seb.sebserver.gui.table.EntityTable;
import ch.ethz.seb.sebserver.gui.table.RemoteListPageSupplier;
import ch.ethz.seb.sebserver.gui.table.TableBuilder;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
@ -383,6 +384,20 @@ public class PageServiceImpl implements PageService {
this, staticList, entityType);
}
@Override
public <T> TableBuilder<T> remoteListTableBuilder(
final RestCall<Collection<T>> apiCall,
final EntityType entityType) {
return new TableBuilder<>(
(entityType != null)
? entityType.name()
: "",
this,
new RemoteListPageSupplier<>(apiCall, entityType),
entityType);
}
@Override
public boolean logout(final PageContext pageContext) {
this.clearState();

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2020 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.type.TypeReference;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
@Lazy
@Component
@GuiProfile
public class ConfirmPendingClientNotification extends RestCall<Void> {
public ConfirmPendingClientNotification() {
super(new TypeKey<>(
CallType.UNDEFINED,
EntityType.CLIENT_EVENT,
new TypeReference<Void>() {
}),
HttpMethod.POST,
MediaType.APPLICATION_JSON_UTF8,
API.EXAM_MONITORING_ENDPOINT
+ API.PARENT_MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_MONITORING_NOTIFICATION_ENDPOINT
+ API.MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT);
}
}

View file

@ -0,0 +1,45 @@
/*
* Copyright (c) 2020 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session;
import java.util.Collection;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.type.TypeReference;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
@Lazy
@Component
@GuiProfile
public class GetPendingClientNotifications extends RestCall<Collection<ClientEvent>> {
public GetPendingClientNotifications() {
super(new TypeKey<>(
CallType.GET_LIST,
EntityType.CLIENT_EVENT,
new TypeReference<Collection<ClientEvent>>() {
}),
HttpMethod.GET,
MediaType.APPLICATION_FORM_URLENCODED,
API.EXAM_MONITORING_ENDPOINT
+ API.PARENT_MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_MONITORING_NOTIFICATION_ENDPOINT
+ API.EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT);
}
}

View file

@ -0,0 +1,118 @@
/*
* Copyright (c) 2020 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.gui.table;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import org.springframework.util.MultiValueMap;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.Page;
import ch.ethz.seb.sebserver.gbl.model.PageSortOrder;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
public class RemoteListPageSupplier<T> implements PageSupplier<T> {
private final EntityType entityType;
private final RestCall<Collection<T>> restCall;
public RemoteListPageSupplier(final RestCall<Collection<T>> restCall, final EntityType entityType) {
this.restCall = restCall;
this.entityType = entityType;
}
@Override
public EntityType getEntityType() {
return this.entityType;
}
@Override
public Builder<T> newBuilder() {
return new StaticListTableBuilderAdapter<>(this.restCall);
}
public static final class StaticListTableBuilderAdapter<T> implements Builder<T> {
private final RestCall<Collection<T>>.RestCallBuilder restCallBuilder;
private int pageNumber;
private int pageSize;
private String column;
private StaticListTableBuilderAdapter(final RestCall<Collection<T>> restCall) {
this.restCallBuilder = restCall.newBuilder();
}
@Override
public Builder<T> withPaging(final int pageNumber, final int pageSize) {
this.pageNumber = pageNumber;
this.pageSize = pageSize;
return this;
}
@Override
public Builder<T> withSorting(final String column, final PageSortOrder order) {
this.restCallBuilder.withSorting(column, order);
this.column = column;
return this;
}
@Override
public Builder<T> withQueryParams(final MultiValueMap<String, String> params) {
this.restCallBuilder.withQueryParams(params);
return this;
}
@Override
public Builder<T> withQueryParam(final String name, final String value) {
this.restCallBuilder.withQueryParam(name, value);
return this;
}
@Override
public Builder<T> withURIVariable(final String name, final String id) {
this.restCallBuilder.withURIVariable(name, id);
return this;
}
@Override
public Builder<T> apply(final Function<Builder<T>, Builder<T>> f) {
return f.apply(this);
}
@Override
public Result<Page<T>> getPage() {
return Result.tryCatch(() -> {
final Collection<T> collection = this.restCallBuilder.call().getOrThrow();
final List<T> list = (collection == null || collection.isEmpty())
? Collections.emptyList()
: new ArrayList<>(collection);
if (list.isEmpty()) {
return new Page<>(0, this.pageNumber, this.column, list);
}
if (this.pageSize <= 0) {
return new Page<>(1, 1, this.column, list);
}
final int numOfPages = list.size() / this.pageSize;
final List<T> subList = list.subList(this.pageNumber * this.pageSize,
this.pageNumber * this.pageSize + this.pageSize);
return new Page<>(numOfPages, this.pageNumber, this.column, subList);
});
}
}
}

View file

@ -35,7 +35,7 @@ public class TableBuilder<ROW> {
private final String name;
private final PageService pageService;
final RestCall<Page<ROW>> restCall;
final List<ROW> staticList;
final PageSupplier<ROW> pageSupplier;
final EntityType entityType;
private final MultiValueMap<String, String> staticQueryParams;
final List<ColumnDefinition<ROW>> columns = new ArrayList<>();
@ -58,7 +58,7 @@ public class TableBuilder<ROW> {
this.name = name;
this.pageService = pageService;
this.restCall = restCall;
this.staticList = null;
this.pageSupplier = null;
this.entityType = null;
this.staticQueryParams = new LinkedMultiValueMap<>();
}
@ -72,8 +72,22 @@ public class TableBuilder<ROW> {
this.name = name;
this.pageService = pageService;
this.restCall = null;
this.staticList = staticList;
this.entityType = entityType;
this.pageSupplier = new StaticListPageSupplier<>(staticList, entityType);
this.staticQueryParams = new LinkedMultiValueMap<>();
}
public TableBuilder(
final String name,
final PageService pageService,
final PageSupplier<ROW> pageSupplier,
final EntityType entityType) {
this.name = name;
this.pageService = pageService;
this.restCall = null;
this.entityType = entityType;
this.pageSupplier = pageSupplier;
this.staticQueryParams = new LinkedMultiValueMap<>();
}
@ -184,7 +198,7 @@ public class TableBuilder<ROW> {
pageContext,
(this.restCall != null)
? new RestCallPageSupplier<>(this.restCall)
: new StaticListPageSupplier<>(this.staticList, this.entityType),
: this.pageSupplier,
this.restCallAdapter,
this.pageService,
this.columns,

View file

@ -1522,6 +1522,12 @@ sebserver.monitoring.exam.connection.action.hide.undefined=Hide Undefined
sebserver.monitoring.exam.connection.action.show.undefined=Show Undefined
sebserver.monitoring.exam.connection.action.proctoring=Single Room Proctoring
sebserver.monitoring.exam.connection.action.proctoring.examroom=Exam Room Proctoring
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.
sebserver.monitoring.exam.connection.notificationlist.title=Pending Notification
sebserver.monitoring.exam.connection.notificationlist.title.tooltip=All pending notifications sent by the SEB Client
sebserver.monitoring.exam.connection.eventlist.title=Events
sebserver.monitoring.exam.connection.eventlist.title.tooltip=All events and logs sent by the SEB Client