GUACAMOLE-394: Merge add interface for browsing user access history.

This commit is contained in:
Nick Couchman
2018-01-05 10:33:56 -05:00
19 changed files with 443 additions and 116 deletions

View File

@@ -20,6 +20,7 @@
package org.apache.guacamole.rest.connection; package org.apache.guacamole.rest.connection;
import java.util.Collection; import java.util.Collection;
import java.util.Date;
import java.util.Map; import java.util.Map;
import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.codehaus.jackson.annotate.JsonIgnoreProperties;
import org.codehaus.jackson.map.annotate.JsonSerialize; import org.codehaus.jackson.map.annotate.JsonSerialize;
@@ -75,7 +76,13 @@ public class APIConnection {
* The count of currently active connections using this connection. * The count of currently active connections using this connection.
*/ */
private int activeConnections; private int activeConnections;
/**
* The date and time that this connection was last used, or null if this
* connection has never been used or this information is unavailable.
*/
private Date lastActive;
/** /**
* Create an empty APIConnection. * Create an empty APIConnection.
*/ */
@@ -97,6 +104,7 @@ public class APIConnection {
this.identifier = connection.getIdentifier(); this.identifier = connection.getIdentifier();
this.parentIdentifier = connection.getParentIdentifier(); this.parentIdentifier = connection.getParentIdentifier();
this.activeConnections = connection.getActiveConnections(); this.activeConnections = connection.getActiveConnections();
this.lastActive = connection.getLastActive();
// Set protocol from configuration // Set protocol from configuration
GuacamoleConfiguration configuration = connection.getConfiguration(); GuacamoleConfiguration configuration = connection.getConfiguration();
@@ -257,4 +265,27 @@ public class APIConnection {
this.sharingProfiles = sharingProfiles; this.sharingProfiles = sharingProfiles;
} }
/**
* Returns the date and time that this connection was last used, or null if
* this connection has never been used or this information is unavailable.
*
* @return
* The date and time that this connection was last used, or null if this
* connection has never been used or this information is unavailable.
*/
public Date getLastActive() {
return lastActive;
}
/**
* Sets the date and time that this connection was last used.
*
* @param lastActive
* The date and time that this connection was last used, or null if this
* connection has never been used or this information is unavailable.
*/
public void setLastActive(Date lastActive) {
this.lastActive = lastActive;
}
} }

View File

@@ -0,0 +1,131 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.rest.history;
import java.util.Date;
import org.apache.guacamole.net.auth.ActivityRecord;
/**
* A activity record which may be exposed through the REST endpoints.
*/
public class APIActivityRecord {
/**
* The date and time the activity began.
*/
private final Date startDate;
/**
* The date and time the activity ended, or null if the activity is
* still in progress or if the end time is unknown.
*/
private final Date endDate;
/**
* The hostname or IP address of the remote host that performed the
* activity associated with this record, if known.
*/
private final String remoteHost;
/**
* The name of the user who performed or is performing the activity
* associated with this record.
*/
private final String username;
/**
* Whether the activity is still in progress.
*/
private final boolean active;
/**
* Creates a new APIActivityRecord, copying the data from the given activity
* record.
*
* @param record
* The record to copy data from.
*/
public APIActivityRecord(ActivityRecord record) {
this.startDate = record.getStartDate();
this.endDate = record.getEndDate();
this.remoteHost = record.getRemoteHost();
this.username = record.getUsername();
this.active = record.isActive();
}
/**
* Returns the date and time the activity began.
*
* @return
* The date and time the activity began.
*/
public Date getStartDate() {
return startDate;
}
/**
* Returns the date and time the activity ended, if applicable.
*
* @return
* The date and time the activity ended, or null if the activity is
* still in progress or if the end time is unknown.
*/
public Date getEndDate() {
return endDate;
}
/**
* Returns the hostname or IP address of the remote host that performed the
* activity associated with this record, if known.
*
* @return
* The hostname or IP address of the remote host that performed the
* activity associated with this record, or null if the remote host is
* unknown.
*/
public String getRemoteHost() {
return remoteHost;
}
/**
* Returns the name of the user who performed or is performing the activity
* associated with this record.
*
* @return
* The name of the user who performed or is performing the activity
* associated with this record.
*/
public String getUsername() {
return username;
}
/**
* Returns whether the activity associated with this record is still in
* progress.
*
* @return
* true if the activity associated with this record is still in
* progress, false otherwise.
*/
public boolean isActive() {
return active;
}
}

View File

@@ -19,13 +19,12 @@
package org.apache.guacamole.rest.history; package org.apache.guacamole.rest.history;
import java.util.Date;
import org.apache.guacamole.net.auth.ConnectionRecord; import org.apache.guacamole.net.auth.ConnectionRecord;
/** /**
* A connection record which may be exposed through the REST endpoints. * A connection record which may be exposed through the REST endpoints.
*/ */
public class APIConnectionRecord { public class APIConnectionRecord extends APIActivityRecord {
/** /**
* The identifier of the connection associated with this record. * The identifier of the connection associated with this record.
@@ -47,32 +46,6 @@ public class APIConnectionRecord {
*/ */
private final String sharingProfileName; private final String sharingProfileName;
/**
* The date and time the connection began.
*/
private final Date startDate;
/**
* The date and time the connection ended, or null if the connection is
* still running or if the end time is unknown.
*/
private final Date endDate;
/**
* The host from which the connection originated, if known.
*/
private final String remoteHost;
/**
* The name of the user who used or is using the connection.
*/
private final String username;
/**
* Whether the connection is currently active.
*/
private final boolean active;
/** /**
* Creates a new APIConnectionRecord, copying the data from the given * Creates a new APIConnectionRecord, copying the data from the given
* record. * record.
@@ -81,15 +54,11 @@ public class APIConnectionRecord {
* The record to copy data from. * The record to copy data from.
*/ */
public APIConnectionRecord(ConnectionRecord record) { public APIConnectionRecord(ConnectionRecord record) {
super(record);
this.connectionIdentifier = record.getConnectionIdentifier(); this.connectionIdentifier = record.getConnectionIdentifier();
this.connectionName = record.getConnectionName(); this.connectionName = record.getConnectionName();
this.sharingProfileIdentifier = record.getSharingProfileIdentifier(); this.sharingProfileIdentifier = record.getSharingProfileIdentifier();
this.sharingProfileName = record.getSharingProfileName(); this.sharingProfileName = record.getSharingProfileName();
this.startDate = record.getStartDate();
this.endDate = record.getEndDate();
this.remoteHost = record.getRemoteHost();
this.username = record.getUsername();
this.active = record.isActive();
} }
/** /**
@@ -139,58 +108,4 @@ public class APIConnectionRecord {
return sharingProfileName; return sharingProfileName;
} }
/**
* Returns the date and time the connection began.
*
* @return
* The date and time the connection began.
*/
public Date getStartDate() {
return startDate;
}
/**
* Returns the date and time the connection ended, if applicable.
*
* @return
* The date and time the connection ended, or null if the connection is
* still running or if the end time is unknown.
*/
public Date getEndDate() {
return endDate;
}
/**
* Returns the remote host from which this connection originated.
*
* @return
* The remote host from which this connection originated.
*/
public String getRemoteHost() {
return remoteHost;
}
/**
* Returns the name of the user who used or is using the connection at the
* times given by this connection record.
*
* @return
* The name of the user who used or is using the associated connection.
*/
public String getUsername() {
return username;
}
/**
* Returns whether the connection associated with this record is still
* active.
*
* @return
* true if the connection associated with this record is still active,
* false otherwise.
*/
public boolean isActive() {
return active;
}
} }

View File

@@ -25,10 +25,10 @@ import org.apache.guacamole.net.auth.ActivityRecordSet;
import org.apache.guacamole.rest.APIException; import org.apache.guacamole.rest.APIException;
/** /**
* A sort predicate which species the property to use when sorting connection * A sort predicate which species the property to use when sorting activity
* records, along with the sort order. * records, along with the sort order.
*/ */
public class APIConnectionRecordSortPredicate { public class APISortPredicate {
/** /**
* The prefix which will be included before the name of a sortable property * The prefix which will be included before the name of a sortable property
@@ -43,8 +43,8 @@ public class APIConnectionRecordSortPredicate {
public enum SortableProperty { public enum SortableProperty {
/** /**
* The date that the connection associated with the connection record * The date that the activity associated with the activity record
* began (connected). * began.
*/ */
startDate(ActivityRecordSet.SortableProperty.START_DATE); startDate(ActivityRecordSet.SortableProperty.START_DATE);
@@ -70,7 +70,7 @@ public class APIConnectionRecordSortPredicate {
} }
/** /**
* The property to use when sorting ConnectionRecords. * The property to use when sorting ActivityRecords.
*/ */
private ActivityRecordSet.SortableProperty property; private ActivityRecordSet.SortableProperty property;
@@ -93,7 +93,7 @@ public class APIConnectionRecordSortPredicate {
* @throws APIException * @throws APIException
* If the provided sort predicate string is invalid. * If the provided sort predicate string is invalid.
*/ */
public APIConnectionRecordSortPredicate(String value) public APISortPredicate(String value)
throws APIException { throws APIException {
// Parse whether sort order is descending // Parse whether sort order is descending
@@ -124,7 +124,7 @@ public class APIConnectionRecordSortPredicate {
* @return * @return
* The ActivityRecordSet.SortableProperty which refers to the same * The ActivityRecordSet.SortableProperty which refers to the same
* property as the string originally provided when this * property as the string originally provided when this
* APIConnectionRecordSortPredicate was created. * APISortPredicate was created.
*/ */
public ActivityRecordSet.SortableProperty getProperty() { public ActivityRecordSet.SortableProperty getProperty() {
return property; return property;

View File

@@ -28,6 +28,7 @@ import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam; import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.auth.ActivityRecord;
import org.apache.guacamole.net.auth.ActivityRecordSet; import org.apache.guacamole.net.auth.ActivityRecordSet;
import org.apache.guacamole.net.auth.ConnectionRecord; import org.apache.guacamole.net.auth.ConnectionRecord;
import org.apache.guacamole.net.auth.UserContext; import org.apache.guacamole.net.auth.UserContext;
@@ -88,7 +89,7 @@ public class HistoryResource {
@Path("connections") @Path("connections")
public List<APIConnectionRecord> getConnectionHistory( public List<APIConnectionRecord> getConnectionHistory(
@QueryParam("contains") List<String> requiredContents, @QueryParam("contains") List<String> requiredContents,
@QueryParam("order") List<APIConnectionRecordSortPredicate> sortPredicates) @QueryParam("order") List<APISortPredicate> sortPredicates)
throws GuacamoleException { throws GuacamoleException {
// Retrieve overall connection history // Retrieve overall connection history
@@ -101,7 +102,7 @@ public class HistoryResource {
} }
// Sort according to specified ordering // Sort according to specified ordering
for (APIConnectionRecordSortPredicate predicate : sortPredicates) for (APISortPredicate predicate : sortPredicates)
history = history.sort(predicate.getProperty(), predicate.isDescending()); history = history.sort(predicate.getProperty(), predicate.isDescending());
// Limit to maximum result size // Limit to maximum result size
@@ -117,4 +118,59 @@ public class HistoryResource {
} }
/**
* Retrieves the login history for all users, restricted by optional filter
* parameters.
*
* @param requiredContents
* The set of strings that each must occur somewhere within the
* returned user records, whether within the associated username or any
* associated date. If non-empty, any user record not matching each of
* the strings within the collection will be excluded from the results.
*
* @param sortPredicates
* A list of predicates to apply while sorting the resulting user
* records, describing the properties involved and the sort order for
* those properties.
*
* @return
* A list of user records, describing the start and end times of user
* sessions.
*
* @throws GuacamoleException
* If an error occurs while retrieving the user history.
*/
@GET
@Path("users")
public List<APIActivityRecord> getUserHistory(
@QueryParam("contains") List<String> requiredContents,
@QueryParam("order") List<APISortPredicate> sortPredicates)
throws GuacamoleException {
// Retrieve overall user history
ActivityRecordSet<ActivityRecord> history = userContext.getUserHistory();
// Restrict to records which contain the specified strings
for (String required : requiredContents) {
if (!required.isEmpty())
history = history.contains(required);
}
// Sort according to specified ordering
for (APISortPredicate predicate : sortPredicates)
history = history.sort(predicate.getProperty(), predicate.isDescending());
// Limit to maximum result size
history = history.limit(MAXIMUM_HISTORY_SIZE);
// Convert record set to collection of API user records
List<APIActivityRecord> apiRecords = new ArrayList<APIActivityRecord>();
for (ActivityRecord record : history.asCollection())
apiRecords.add(new APIActivityRecord(record));
// Return the converted history
return apiRecords;
}
} }

View File

@@ -19,6 +19,7 @@
package org.apache.guacamole.rest.user; package org.apache.guacamole.rest.user;
import java.util.Date;
import java.util.Map; import java.util.Map;
import org.codehaus.jackson.annotate.JsonIgnoreProperties; import org.codehaus.jackson.annotate.JsonIgnoreProperties;
import org.codehaus.jackson.map.annotate.JsonSerialize; import org.codehaus.jackson.map.annotate.JsonSerialize;
@@ -46,6 +47,12 @@ public class APIUser {
*/ */
private Map<String, String> attributes; private Map<String, String> attributes;
/**
* The date and time that this user was last logged in, or null if this user
* has never logged in or this information is unavailable.
*/
private Date lastActive;
/** /**
* Construct a new empty APIUser. * Construct a new empty APIUser.
*/ */
@@ -60,6 +67,7 @@ public class APIUser {
// Set user information // Set user information
this.username = user.getIdentifier(); this.username = user.getIdentifier();
this.password = user.getPassword(); this.password = user.getPassword();
this.lastActive = user.getLastActive();
// Associate any attributes // Associate any attributes
this.attributes = user.getAttributes(); this.attributes = user.getAttributes();
@@ -122,4 +130,27 @@ public class APIUser {
this.attributes = attributes; this.attributes = attributes;
} }
/**
* Returns the date and time that this user was last logged in, or null if
* this user has never logged in or this information is unavailable.
*
* @return
* The date and time that this user was last logged in, or null if this
* user has never logged in or this information is unavailable.
*/
public Date getLastActive() {
return lastActive;
}
/**
* Sets the date and time that this user was last logged in.
*
* @param lastActive
* The date and time that this user was last logged in, or null if this
* user has never logged in or this information is unavailable.
*/
public void setLastActive(Date lastActive) {
this.lastActive = lastActive;
}
} }

View File

@@ -21,8 +21,11 @@ package org.apache.guacamole.rest.user;
import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject; import com.google.inject.assistedinject.AssistedInject;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.PUT; import javax.ws.rs.PUT;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
@@ -30,6 +33,7 @@ import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleSecurityException; import org.apache.guacamole.GuacamoleSecurityException;
import org.apache.guacamole.net.auth.ActivityRecord;
import org.apache.guacamole.net.auth.AuthenticationProvider; import org.apache.guacamole.net.auth.AuthenticationProvider;
import org.apache.guacamole.net.auth.Credentials; import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.net.auth.User; import org.apache.guacamole.net.auth.User;
@@ -38,6 +42,7 @@ import org.apache.guacamole.net.auth.UserContext;
import org.apache.guacamole.net.auth.credentials.GuacamoleCredentialsException; import org.apache.guacamole.net.auth.credentials.GuacamoleCredentialsException;
import org.apache.guacamole.rest.directory.DirectoryObjectResource; import org.apache.guacamole.rest.directory.DirectoryObjectResource;
import org.apache.guacamole.rest.directory.DirectoryObjectTranslator; import org.apache.guacamole.rest.directory.DirectoryObjectTranslator;
import org.apache.guacamole.rest.history.APIActivityRecord;
import org.apache.guacamole.rest.permission.PermissionSetResource; import org.apache.guacamole.rest.permission.PermissionSetResource;
/** /**
@@ -93,6 +98,31 @@ public class UserResource
this.user = user; this.user = user;
} }
/**
* Retrieves the login (session) history of a single user.
*
* @return
* A list of activity records, describing the start and end times of
* this user's sessions.
*
* @throws GuacamoleException
* If an error occurs while retrieving the user history.
*/
@GET
@Path("history")
public List<APIActivityRecord> getUserHistory()
throws GuacamoleException {
// Retrieve the requested user's history
List<APIActivityRecord> apiRecords = new ArrayList<APIActivityRecord>();
for (ActivityRecord record : user.getHistory())
apiRecords.add(new APIActivityRecord(record));
// Return the converted history
return apiRecords;
}
@Override @Override
public void updateObject(APIUser modifiedObject) throws GuacamoleException { public void updateObject(APIUser modifiedObject) throws GuacamoleException {

View File

@@ -104,6 +104,15 @@ angular.module('rest').factory('Connection', [function defineConnection() {
*/ */
this.sharingProfiles = template.sharingProfiles; this.sharingProfiles = template.sharingProfiles;
/**
* The time that this connection was last used, in milliseconds since
* 1970-01-01 00:00:00 UTC. If this information is unknown or
* unavailable, this will be null.
*
* @type Number
*/
this.lastActive = template.lastActive;
}; };
return Connection; return Connection;

View File

@@ -53,6 +53,15 @@ angular.module('rest').factory('User', [function defineUser() {
*/ */
this.password = template.password; this.password = template.password;
/**
* The time that this user was last logged in, in milliseconds since
* 1970-01-01 00:00:00 UTC. If this information is unknown or
* unavailable, this will be null.
*
* @type Number
*/
this.lastActive = template.lastActive;
/** /**
* Arbitrary name/value pairs which further describe this user. The * Arbitrary name/value pairs which further describe this user. The
* semantics and validity of these attributes are dictated by the * semantics and validity of these attributes are dictated by the

View File

@@ -36,9 +36,11 @@ angular.module('settings').directive('guacSettingsUsers', [function guacSettings
// Required types // Required types
var ManageableUser = $injector.get('ManageableUser'); var ManageableUser = $injector.get('ManageableUser');
var PermissionSet = $injector.get('PermissionSet'); var PermissionSet = $injector.get('PermissionSet');
var SortOrder = $injector.get('SortOrder');
// Required services // Required services
var $location = $injector.get('$location'); var $location = $injector.get('$location');
var $translate = $injector.get('$translate');
var authenticationService = $injector.get('authenticationService'); var authenticationService = $injector.get('authenticationService');
var dataSourceService = $injector.get('dataSourceService'); var dataSourceService = $injector.get('dataSourceService');
var guacNotification = $injector.get('guacNotification'); var guacNotification = $injector.get('guacNotification');
@@ -98,9 +100,37 @@ angular.module('settings').directive('guacSettingsUsers', [function guacSettings
* @type String[] * @type String[]
*/ */
$scope.filteredUserProperties = [ $scope.filteredUserProperties = [
'user.lastActive',
'user.username' 'user.username'
]; ];
/**
* The date format for use for the last active date.
*
* @type String
*/
$scope.dateFormat = null;
/**
* SortOrder instance which stores the sort order of the listed
* users.
*
* @type SortOrder
*/
$scope.order = new SortOrder([
'user.username',
'-user.lastActive'
]);
// Get session date format
$translate('SETTINGS_USERS.FORMAT_DATE')
.then(function dateFormatReceived(retrievedDateFormat) {
// Store received date format
$scope.dateFormat = retrievedDateFormat;
});
/** /**
* Returns whether critical data has completed being loaded. * Returns whether critical data has completed being loaded.
* *
@@ -110,7 +140,8 @@ angular.module('settings').directive('guacSettingsUsers', [function guacSettings
*/ */
$scope.isLoaded = function isLoaded() { $scope.isLoaded = function isLoaded() {
return $scope.manageableUsers !== null return $scope.dateFormat !== null
&& $scope.manageableUsers !== null
&& $scope.permissions !== null; && $scope.permissions !== null;
}; };

View File

@@ -0,0 +1,42 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.settings.users table.user-list {
width: 100%;
}
.settings.users table.user-list th.last-active,
.settings.users table.user-list td.last-active {
white-space: nowrap;
width: 0;
}
.settings.users table.user-list th.username,
.settings.users table.user-list td.username {
width: 100%;
}
.settings.users table.user-list tr.user td.username a[href] {
display: block;
padding: .5em 1em;
}
.settings.users table.user-list tr.user td.username {
padding: 0;
}

View File

@@ -21,19 +21,32 @@
</div> </div>
<!-- List of users this user has access to --> <!-- List of users this user has access to -->
<div class="user-list"> <table class="sorted user-list">
<div ng-repeat="manageableUser in manageableUserPage" class="user list-item"> <thead>
<a ng-href="#/manage/{{manageableUser.dataSource}}/users/{{manageableUser.user.username}}"> <tr>
<div class="caption"> <th guac-sort-order="order" guac-sort-property="'user.username'" class="username">
<div class="icon user"></div> {{'SETTINGS_USERS.TABLE_HEADER_USERNAME' | translate}}
<span class="name">{{manageableUser.user.username}}</span> </th>
</div> <th guac-sort-order="order" guac-sort-property="'user.lastActive'" class="last-active">
</a> {{'SETTINGS_USERS.TABLE_HEADER_LAST_ACTIVE' | translate}}
</div> </th>
</div> </tr>
</thead>
<tbody ng-class="{loading: !isLoaded()}">
<tr ng-repeat="manageableUser in manageableUserPage" class="user">
<td class="username">
<a ng-href="#/manage/{{manageableUser.dataSource}}/users/{{manageableUser.user.username}}">
<div class="icon user"></div>
<span class="name">{{manageableUser.user.username}}</span>
</a>
</td>
<td class="last-active">{{manageableUser.user.lastActive | date : dateFormat}}</td>
</tr>
</tbody>
</table>
<!-- Pager controls for user list --> <!-- Pager controls for user list -->
<guac-pager page="manageableUserPage" page-size="25" <guac-pager page="manageableUserPage" page-size="25"
items="filteredManageableUsers | orderBy : 'user.username'"></guac-pager> items="filteredManageableUsers | orderBy : order.predicate"></guac-pager>
</div> </div>

View File

@@ -598,9 +598,13 @@
"FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
"FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",
"HELP_USERS" : "Klicke oder Tippe auf einen Benutzer um diesen zu verwalten. Abhänig von Ihrer Zugriffsebene können Benutzer hinzugefügt, gelöscht bzw. dessen Passwort geändert werden.", "HELP_USERS" : "Klicke oder Tippe auf einen Benutzer um diesen zu verwalten. Abhänig von Ihrer Zugriffsebene können Benutzer hinzugefügt, gelöscht bzw. dessen Passwort geändert werden.",
"SECTION_HEADER_USERS" : "Benutzer" "SECTION_HEADER_USERS" : "Benutzer",
"TABLE_HEADER_USERNAME" : "Benutzername"
}, },

View File

@@ -675,9 +675,14 @@
"FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
"FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",
"HELP_USERS" : "Click or tap on a user below to manage that user. Depending on your access level, users can be added and deleted, and their passwords can be changed.", "HELP_USERS" : "Click or tap on a user below to manage that user. Depending on your access level, users can be added and deleted, and their passwords can be changed.",
"SECTION_HEADER_USERS" : "Users" "SECTION_HEADER_USERS" : "Users",
"TABLE_HEADER_LAST_ACTIVE" : "Last active",
"TABLE_HEADER_USERNAME" : "Username"
}, },

View File

@@ -601,9 +601,13 @@
"FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
"FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",
"HELP_USERS" : "Cliquer ou appuyer sur un utilisateur en dessous pour le gérer. Selon vos permissions, les utilisateurs peuvent être ajoutés, supprimés, leur mot de passe changé.", "HELP_USERS" : "Cliquer ou appuyer sur un utilisateur en dessous pour le gérer. Selon vos permissions, les utilisateurs peuvent être ajoutés, supprimés, leur mot de passe changé.",
"SECTION_HEADER_USERS" : "Utilisateur" "SECTION_HEADER_USERS" : "Utilisateur",
"TABLE_HEADER_USERNAME" : "Identifiant"
}, },

View File

@@ -543,9 +543,13 @@
"FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
"FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",
"HELP_USERS" : "Click or tap on a user below to manage that user. Depending on your access level, users can be added and deleted, and their passwords can be changed.", "HELP_USERS" : "Click or tap on a user below to manage that user. Depending on your access level, users can be added and deleted, and their passwords can be changed.",
"SECTION_HEADER_USERS" : "Utenti" "SECTION_HEADER_USERS" : "Utenti",
"TABLE_HEADER_USERNAME" : "Username"
}, },

View File

@@ -631,9 +631,13 @@
"FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
"FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",
"HELP_USERS" : "Klik of tik op een van de onderstaande gebruikers om die te beheren. Afhankelijk van uw toegangsniveau kunnen gebruikers worden toegevoegd, verwijderd en hun wachtwoorden gewijzigd.", "HELP_USERS" : "Klik of tik op een van de onderstaande gebruikers om die te beheren. Afhankelijk van uw toegangsniveau kunnen gebruikers worden toegevoegd, verwijderd en hun wachtwoorden gewijzigd.",
"SECTION_HEADER_USERS" : "Gebruikers" "SECTION_HEADER_USERS" : "Gebruikers",
"TABLE_HEADER_USERNAME" : "Gebruikersnaam"
}, },

View File

@@ -612,9 +612,13 @@
"FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
"FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",
"HELP_USERS" : "Klikk på en bruker under for å administrere den brukeren. Avhengig av din tilgang kan brukere legges til, slettes og passordet kan endres.", "HELP_USERS" : "Klikk på en bruker under for å administrere den brukeren. Avhengig av din tilgang kan brukere legges til, slettes og passordet kan endres.",
"SECTION_HEADER_USERS" : "Brukere" "SECTION_HEADER_USERS" : "Brukere",
"TABLE_HEADER_USERNAME" : "Brukernavn"
}, },

View File

@@ -524,9 +524,13 @@
"FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
"FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",
"HELP_USERS" : "Нажмите на пользователя, чтобы управлять им. В зависимости от прав доступа возможно добавление и удаление пользователей, а также изменение паролей.", "HELP_USERS" : "Нажмите на пользователя, чтобы управлять им. В зависимости от прав доступа возможно добавление и удаление пользователей, а также изменение паролей.",
"SECTION_HEADER_USERS" : "Пользователи" "SECTION_HEADER_USERS" : "Пользователи",
"TABLE_HEADER_USERNAME" : "Имя пользователя"
}, },