SEBSERV-139 implementing GUI

This commit is contained in:
anhefti 2020-08-06 17:01:38 +02:00
parent 6eedcbb4a0
commit 548d4d132f
13 changed files with 261 additions and 17 deletions

View file

@ -126,7 +126,6 @@ public final class API {
public static final String EXAM_ADMINISTRATION_CHECK_RESTRICTION_PATH_SEGMENT = "/check-seb-restriction";
public static final String EXAM_ADMINISTRATION_CHECK_IMPORTED_PATH_SEGMENT = "/check-imported";
public static final String EXAM_ADMINISTRATION_SEB_RESTRICTION_CHAPTERS_PATH_SEGMENT = "/chapters";
public static final String EXAM_ADMINISTRATION_PROCTOR_PATH_SEGMENT = "/proctoring";
public static final String EXAM_INDICATOR_ENDPOINT = "/indicator";

View file

@ -18,8 +18,10 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.Entity;
import ch.ethz.seb.sebserver.webservice.servicelayer.validation.ValidProctoringSettings;
@JsonIgnoreProperties(ignoreUnknown = true)
@ValidProctoringSettings
public class ProctoringSettings implements Entity {
public enum ServerType {
@ -42,7 +44,7 @@ public class ProctoringSettings implements Entity {
public final ServerType serverType;
@JsonProperty(ATTR_SERVER_URL)
@URL(message = "examProctoring:serverURL:invalidURL")
@URL(message = "proctoringSettings:serverURL:invalidURL")
public final String serverURL;
@JsonProperty(ATTR_APP_KEY)

View file

@ -33,6 +33,7 @@ import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
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.exam.ProctoringSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult.ErrorType;
@ -57,6 +58,7 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.CheckExamConsistency;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.CheckSEBRestriction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetProctoringSettings;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.SaveExam;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.GetLmsSetup;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.TestLmsSetup;
@ -119,6 +121,7 @@ public class ExamForm implements TemplateComposer {
private final PageService pageService;
private final ResourceService resourceService;
private final ExamSEBRestrictionSettings examSEBRestrictionSettings;
private final ExamProctoringSettings examProctoringSettings;
private final WidgetFactory widgetFactory;
private final RestService restService;
private final ExamDeletePopup examDeletePopup;
@ -128,6 +131,7 @@ public class ExamForm implements TemplateComposer {
protected ExamForm(
final PageService pageService,
final ExamSEBRestrictionSettings examSEBRestrictionSettings,
final ExamProctoringSettings examProctoringSettings,
final ExamToConfigBindingForm examToConfigBindingForm,
final DownloadService downloadService,
final ExamDeletePopup examDeletePopup,
@ -137,6 +141,7 @@ public class ExamForm implements TemplateComposer {
this.pageService = pageService;
this.resourceService = pageService.getResourceService();
this.examSEBRestrictionSettings = examSEBRestrictionSettings;
this.examProctoringSettings = examProctoringSettings;
this.widgetFactory = pageService.getWidgetFactory();
this.restService = this.resourceService.getRestService();
this.examDeletePopup = examDeletePopup;
@ -336,6 +341,13 @@ public class ExamForm implements TemplateComposer {
? this.restService.getRestCall(ImportAsExam.class)
: this.restService.getRestCall(SaveExam.class));
final boolean proctoringEnabled = this.restService
.getBuilder(GetProctoringSettings.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.call()
.map(ProctoringSettings::getEnableProctoring)
.getOr(false);
final PageActionBuilder actionBuilder = this.pageService.pageActionBuilder(formContext
.clearEntityKeys()
.removeAttribute(AttributeKeys.IMPORT_FROM_QUIZ_DATA));
@ -384,6 +396,18 @@ public class ExamForm implements TemplateComposer {
.publishIf(() -> sebRestrictionAvailable && readonly && modifyGrant && !importFromQuizData
&& BooleanUtils.isTrue(isRestricted))
.newAction(ActionDefinition.EXAM_PROCTORING_ON)
.withEntityKey(entityKey)
.withExec(this.examProctoringSettings.settingsFunction(this.pageService))
.noEventPropagation()
.publishIf(() -> proctoringEnabled && readonly)
.newAction(ActionDefinition.EXAM_PROCTORING_OFF)
.withEntityKey(entityKey)
.withExec(this.examProctoringSettings.settingsFunction(this.pageService))
.noEventPropagation()
.publishIf(() -> !proctoringEnabled && readonly)
.newAction(ActionDefinition.EXAM_DELETE)
.withEntityKey(entityKey)
.withExec(this.examDeletePopup.deleteWizardFunction(pageContext))

View file

@ -16,12 +16,16 @@ import org.apache.commons.lang3.BooleanUtils;
import org.eclipse.swt.widgets.Composite;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings.ServerType;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
import ch.ethz.seb.sebserver.gui.form.Form;
import ch.ethz.seb.sebserver.gui.form.FormBuilder;
import ch.ethz.seb.sebserver.gui.form.FormHandle;
@ -30,12 +34,16 @@ import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.page.ModalInputDialogComposer;
import ch.ethz.seb.sebserver.gui.service.page.PageContext;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.event.ActionEvent;
import ch.ethz.seb.sebserver.gui.service.page.impl.ModalInputDialog;
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetProctoringSettings;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.SaveProctoringSettings;
@Lazy
@Component
@GuiProfile
public class ExamProctoringSettings {
private static final Logger log = LoggerFactory.getLogger(ExamProctoringSettings.class);
@ -55,10 +63,6 @@ public class ExamProctoringSettings {
private final static LocTextKey SEB_PROCTORING_FORM_SECRET =
new LocTextKey("sebserver.exam.proctoring.form.secret");
public ExamProctoringSettings() {
// TODO Auto-generated constructor stub
}
Function<PageAction, PageAction> settingsFunction(final PageService pageService) {
return action -> {
@ -75,7 +79,7 @@ public class ExamProctoringSettings {
pageService,
action.pageContext());
final Predicate<FormHandle<?>> doBind = formHandle -> doCreate(
final Predicate<FormHandle<?>> doBind = formHandle -> doSaveSettings(
pageService,
pageContext,
formHandle);
@ -90,7 +94,7 @@ public class ExamProctoringSettings {
};
}
private boolean doCreate(
private boolean doSaveSettings(
final PageService pageService,
final PageContext pageContext,
final FormHandle<?> formHandle) {
@ -105,6 +109,8 @@ public class ExamProctoringSettings {
ProctoringSettings examProctoring = null;
try {
final Form form = formHandle.getForm();
form.clearErrors();
final boolean enabled = BooleanUtils.toBoolean(
form.getFieldValue(ProctoringSettings.ATTR_ENABLE_PROCTORING));
final ServerType serverType = ServerType.valueOf(
@ -126,7 +132,7 @@ public class ExamProctoringSettings {
return false;
}
return !pageService
final boolean saveOk = !pageService
.getRestService()
.getBuilder(SaveProctoringSettings.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
@ -134,6 +140,19 @@ public class ExamProctoringSettings {
.call()
.onError(formHandle::handleError)
.hasError();
if (saveOk) {
final PageAction action = pageService.pageActionBuilder(pageContext)
.newAction(ActionDefinition.EXAM_VIEW_FROM_LIST)
.create();
pageService.firePageEvent(
new ActionEvent(action),
action.pageContext());
return true;
}
return false;
}
private final class SEBProctoringPropertiesForm
@ -196,9 +215,24 @@ public class ExamProctoringSettings {
ProctoringSettings.ATTR_SERVER_TYPE,
SEB_PROCTORING_FORM_TYPE,
proctoringSettings.serverType.name(),
this.pageService.getResourceService()::examProctoringTypeResources))
resourceService::examProctoringTypeResources))
// TODO
.addField(FormBuilder.text(
ProctoringSettings.ATTR_SERVER_URL,
SEB_PROCTORING_FORM_URL,
proctoringSettings.serverURL))
.addField(FormBuilder.text(
ProctoringSettings.ATTR_APP_KEY,
SEB_PROCTORING_FORM_APPKEY,
proctoringSettings.appKey))
.addField(FormBuilder.password(
ProctoringSettings.ATTR_APP_SECRET,
SEB_PROCTORING_FORM_SECRET,
(proctoringSettings.appSecret != null)
? String.valueOf(proctoringSettings.appSecret)
: null))
.build();

View file

@ -24,6 +24,7 @@ import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
@ -45,8 +46,10 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
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.GetClientConnectionData;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetProctorURLForClient;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
import ch.ethz.seb.sebserver.gui.service.session.ClientConnectionDetails;
import ch.ethz.seb.sebserver.gui.service.session.InstructionProcessor;
@ -237,6 +240,13 @@ public class MonitoringClientConnection implements TemplateComposer {
.compose(pageContext.copyOf(content));
final boolean proctoringEnabled = restService
.getBuilder(GetProctoringSettings.class)
.withURIVariable(API.PARAM_MODEL_ID, parentEntityKey.modelId)
.call()
.map(ProctoringSettings::getEnableProctoring)
.getOr(false);
actionBuilder
.newAction(ActionDefinition.MONITOR_EXAM_BACK_TO_OVERVIEW)
.withEntityKey(parentEntityKey)
@ -256,15 +266,16 @@ public class MonitoringClientConnection implements TemplateComposer {
connectionData.clientConnection.status == ConnectionStatus.ACTIVE)
.newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION_PROCTORING)
.withExec(this::openProctorScreen)
.withEntityKey(parentEntityKey)
.withExec(action -> this.openProctorScreen(action, connectionToken))
.noEventPropagation()
.publish()
.publishIf(() -> proctoringEnabled)
;
}
private PageAction openProctorScreen(final PageAction action) {
private PageAction openProctorScreen(final PageAction action, final String connectionToken) {
//
// final ProctorDialog dialog = new ProctorDialog(action.pageContext().getParent().getShell());
// dialog.open(EVENT_LIST_TITLE_KEY,
@ -274,6 +285,12 @@ public class MonitoringClientConnection implements TemplateComposer {
// urlLauncher.openURL(
// "https://seb-jitsi.ethz.ch/TestRoomABC?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb250ZXh0Ijp7InVzZXIiOnsiYXZhdGFyIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9qb2huLWRvZSIsIm5hbWUiOiJEaXNwbGF5IE5hbWUiLCJlbWFpbCI6Im5hbWVAZXhhbXBsZS5jb20ifX0sImF1ZCI6InNlYi1qaXRzaSIsImlzcyI6InNlYi1qaXRzaSIsInN1YiI6Im1lZXQuaml0c2kiLCJyb29tIjoiKiJ9.SD9Zs78mMFqxS1tpalPTykYYaubIYsj_406WAOhcqxQ");
final String proctorURL = this.pageService.getRestService().getBuilder(GetProctorURLForClient.class)
.withURIVariable(API.PARAM_MODEL_ID, action.getEntityKey().modelId)
.withURIVariable(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionToken)
.call()
.getOrThrow();
final JavaScriptExecutor javaScriptExecutor = RWT.getClient().getService(JavaScriptExecutor.class);
javaScriptExecutor.execute(
"window.open("

View file

@ -297,6 +297,16 @@ public enum ActionDefinition {
ImageIcon.LOCK,
PageStateDefinitionImpl.EXAM_VIEW,
ActionCategory.FORM),
EXAM_PROCTORING_ON(
new LocTextKey("sebserver.exam.proctoring.actions.open"),
ImageIcon.VISIBILITY,
PageStateDefinitionImpl.EXAM_VIEW,
ActionCategory.FORM),
EXAM_PROCTORING_OFF(
new LocTextKey("sebserver.exam.proctoring.actions.open"),
ImageIcon.VISIBILITY_OFF,
PageStateDefinitionImpl.EXAM_VIEW,
ActionCategory.FORM),
EXAM_CONFIGURATION_NEW(
new LocTextKey("sebserver.exam.configuration.action.list.new"),

View file

@ -39,6 +39,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.IndicatorType;
import ch.ethz.seb.sebserver.gbl.model.exam.OpenEdxSEBRestriction.PermissionComponent;
import ch.ethz.seb.sebserver.gbl.model.exam.OpenEdxSEBRestriction.WhiteListPath;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings.ServerType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.AttributeType;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode;
@ -372,7 +373,7 @@ public class ResourceService {
}
public List<Tuple<String>> examProctoringTypeResources() {
return Arrays.stream(ExamType.values())
return Arrays.stream(ServerType.values())
.map(type -> new Tuple3<>(
type.name(),
this.i18nSupport.getText(EXAM_PROCTORING_TYPE_PREFIX + type.name()),

View file

@ -36,7 +36,7 @@ public class SaveProctoringSettings extends RestCall<Exam> {
MediaType.APPLICATION_JSON_UTF8,
API.EXAM_ADMINISTRATION_ENDPOINT
+ API.MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_ADMINISTRATION_SEB_RESTRICTION_PATH_SEGMENT);
+ API.EXAM_ADMINISTRATION_PROCTOR_PATH_SEGMENT);
}
}

View file

@ -0,0 +1,42 @@
/*
* 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 GetProctorURLForClient extends RestCall<String> {
public GetProctorURLForClient() {
super(new TypeKey<>(
CallType.GET_SINGLE,
EntityType.EXAM_PROCTOR_DATA,
new TypeReference<String>() {
}),
HttpMethod.GET,
MediaType.APPLICATION_JSON_UTF8,
API.EXAM_ADMINISTRATION_ENDPOINT
+ API.MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_ADMINISTRATION_PROCTOR_PATH_SEGMENT
+ API.EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT);
}
}

View file

@ -0,0 +1,81 @@
/*
* 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.webservice.servicelayer.validation;
import java.net.InetAddress;
import java.net.URI;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import org.apache.commons.lang3.StringUtils;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings.ServerType;
public class ProctoringSettingsValidator implements ConstraintValidator<ValidProctoringSettings, ProctoringSettings> {
@Override
public boolean isValid(final ProctoringSettings value, final ConstraintValidatorContext context) {
if (value == null) {
return false;
}
if (value.enableProctoring) {
if (value.serverType == ServerType.JITSI_MEET) {
boolean passed = true;
if (StringUtils.isBlank(value.serverURL)) {
context.disableDefaultConstraintViolation();
context
.buildConstraintViolationWithTemplate("proctoringSettings:serverURL:notNull")
.addPropertyNode("serverURL").addConstraintViolation();
passed = false;
}
try {
if (!InetAddress.getByName(new URI(value.serverURL).getHost()).isReachable(5000)) {
context.disableDefaultConstraintViolation();
context
.buildConstraintViolationWithTemplate("proctoringSettings:serverURL:serverNotAvailable")
.addPropertyNode("serverURL").addConstraintViolation();
passed = false;
}
} catch (final Exception e) {
context.disableDefaultConstraintViolation();
context
.buildConstraintViolationWithTemplate("proctoringSettings:serverURL:serverNotAvailable")
.addPropertyNode("serverURL").addConstraintViolation();
passed = false;
}
if (StringUtils.isBlank(value.appKey)) {
context.disableDefaultConstraintViolation();
context
.buildConstraintViolationWithTemplate("proctoringSettings:appKey:notNull")
.addPropertyNode("appKey").addConstraintViolation();
passed = false;
}
if (StringUtils.isBlank(value.appSecret)) {
context.disableDefaultConstraintViolation();
context
.buildConstraintViolationWithTemplate("proctoringSettings:appSecret:notNull")
.addPropertyNode("appSecret").addConstraintViolation();
passed = false;
}
return passed;
}
}
return true;
}
}

View file

@ -0,0 +1,32 @@
/*
* 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.webservice.servicelayer.validation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = ProctoringSettingsValidator.class)
@Documented
public @interface ValidProctoringSettings {
String message() default "{mandatoryWhenEnabled}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View file

@ -429,7 +429,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
+ API.EXAM_ADMINISTRATION_PROCTOR_PATH_SEGMENT
+ API.EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT,
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
produces = MediaType.TEXT_PLAIN_VALUE)
public String getExamProctoringURL(
@RequestParam(
name = API.PARAM_INSTITUTION_ID,

View file

@ -90,6 +90,7 @@ sebserver.form.validation.fieldError.password.mismatch=The retyped password does
sebserver.form.validation.fieldError.invalidURL=The input does not match the URL pattern.
sebserver.form.validation.fieldError.exists=This name already exists. Please choose another one.
sebserver.form.validation.fieldError.email=Invalid mail address
sebserver.form.validation.fieldError.serverNotAvailable=No service seems to be available within the given URL
sebserver.error.unexpected=Unexpected Error
sebserver.page.message=Information
sebserver.dialog.confirm.title=Confirmation
@ -1396,6 +1397,7 @@ sebserver.monitoring.exam.connection.action.hide.disabled=Hide Canceled
sebserver.monitoring.exam.connection.action.show.disabled=Show Canceled
sebserver.monitoring.exam.connection.action.hide.undefined=Hide Undefined
sebserver.monitoring.exam.connection.action.show.undefined=Show Undefined
sebserver.monitoring.exam.connection.action.proctoring=Proctoring
sebserver.monitoring.exam.connection.eventlist.title=Events
sebserver.monitoring.exam.connection.eventlist.title.tooltip=All events and logs sent by the SEB Client