summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSimon Rettberg2021-11-30 15:27:59 +0100
committerSimon Rettberg2021-11-30 15:27:59 +0100
commit8882210158b7351b494783ecc0a2e8dcfda32bd5 (patch)
tree1f2b5a20a20e6784655f42b6466681c8b148199e
parent[permissionmanager] Fix for PHP 8 (diff)
downloadslx-admin-8882210158b7351b494783ecc0a2e8dcfda32bd5.tar.gz
slx-admin-8882210158b7351b494783ecc0a2e8dcfda32bd5.tar.xz
slx-admin-8882210158b7351b494783ecc0a2e8dcfda32bd5.zip
[locations] Modularize additional column handling
Additional columns are now added through a hook, moving specialized code where it belongs.
-rw-r--r--modules-available/baseconfig/hooks/locations-column.inc.php76
-rw-r--r--modules-available/baseconfig/lang/de/module.json3
-rw-r--r--modules-available/baseconfig/lang/en/module.json3
-rw-r--r--modules-available/baseconfig_bwlp/lang/de/config-variables.json2
-rw-r--r--modules-available/baseconfig_bwlp/lang/en/config-variables.json2
-rw-r--r--modules-available/locations/inc/abstractlocationcolumn.inc.php24
-rw-r--r--modules-available/locations/pages/locations.inc.php170
-rw-r--r--modules-available/locations/style.css12
-rw-r--r--modules-available/locations/templates/locations.html107
-rw-r--r--modules-available/serversetup-bwlp-ipxe/hooks/locations-column.inc.php57
-rw-r--r--modules-available/serversetup-bwlp-ipxe/lang/de/module.json1
-rw-r--r--modules-available/serversetup-bwlp-ipxe/lang/en/module.json1
-rw-r--r--modules-available/statistics/hooks/locations-column.inc.php150
-rw-r--r--modules-available/statistics/lang/de/module.json2
-rw-r--r--modules-available/statistics/lang/en/module.json2
-rw-r--r--modules-available/sysconfig/hooks/locations-column.inc.php57
-rw-r--r--modules-available/sysconfig/lang/de/module.json17
-rw-r--r--modules-available/sysconfig/lang/en/module.json17
18 files changed, 467 insertions, 236 deletions
diff --git a/modules-available/baseconfig/hooks/locations-column.inc.php b/modules-available/baseconfig/hooks/locations-column.inc.php
new file mode 100644
index 00000000..ca30d56e
--- /dev/null
+++ b/modules-available/baseconfig/hooks/locations-column.inc.php
@@ -0,0 +1,76 @@
+<?php
+
+if (!User::hasPermission('.baseconfig.view'))
+ return null;
+
+class BaseconfigLocationColumn extends AbstractLocationColumn
+{
+
+ private $byLoc;
+ private $byMachine;
+
+ public function __construct(array $allowedLocationIds)
+ {
+ if (in_array(0, $allowedLocationIds)) {
+ $extra = 'OR m.locationid IS NULL';
+ } else {
+ $extra = '';
+ }
+ // Count overridden config vars
+ $this->byLoc = Database::queryKeyValueList("SELECT locationid, Count(*) AS cnt
+ FROM `setting_location`
+ WHERE locationid IN (:allowedLocationIds)
+ GROUP BY locationid", compact('allowedLocationIds'));
+ // Confusing because the count might be inaccurate within a branch
+ //$this->propagateFields($locationList, '', 'overriddenVars', 'overriddenClass');
+ // Count machines with overriden var(s)
+ $this->byMachine = Database::queryKeyValueList("SELECT IFNULL(m.locationid, 0), Count(DISTINCT sm.machineuuid) AS cnt
+ FROM setting_machine sm
+ INNER JOIN machine m USING (machineuuid)
+ WHERE (m.locationid IN (:allowedLocationIds) $extra)
+ GROUP BY m.locationid", compact('allowedLocationIds'));
+ // There WHERE statement drops clients without location - but this cannot be displayed by the locations
+ // table anyways as of now - maybe implement some day? Or just encourage everyone to have a root location
+ }
+
+ public function getColumnHtml(int $locationId): string
+ {
+ $ret = '';
+ if ($this->byLoc[$locationId] ?? 0) {
+ $title = htmlspecialchars(Dictionary::translateFileModule('baseconfig', 'module', 'overriden-vars-for-location'));
+ $ret .= <<<EOF
+ <span class="badge" title="{$title}">
+ <span class="glyphicon glyphicon-home"></span> {$this->byLoc[$locationId]}
+ </span>
+EOF;
+ }
+ if ($this->byMachine[$locationId] ?? 0) {
+ $title = htmlspecialchars(Dictionary::translateFileModule('baseconfig', 'module', 'overriden-vars-machines'));
+ $ret .= <<<EOF
+ <span class="badge" title="{$title}">
+ <span class="glyphicon glyphicon-tasks"></span> {$this->byMachine[$locationId]}
+ </span>
+EOF;
+ }
+ return $ret;
+ }
+
+ public function getEditUrl(int $locationId): string
+ {
+ if (!User::hasPermission('.baseconfig.edit', $locationId))
+ return '';
+ return '?do=baseconfig&module=locations&locationid=' . $locationId;
+ }
+
+ public function header(): string
+ {
+ return Dictionary::translateFileModule('baseconfig', 'module', 'location-column-header');
+ }
+
+ public function priority(): int
+ {
+ return 1000;
+ }
+}
+
+return new BaseconfigLocationColumn($allowedLocationIds); \ No newline at end of file
diff --git a/modules-available/baseconfig/lang/de/module.json b/modules-available/baseconfig/lang/de/module.json
index f7dbf53a..a9c2c6bf 100644
--- a/modules-available/baseconfig/lang/de/module.json
+++ b/modules-available/baseconfig/lang/de/module.json
@@ -1,5 +1,8 @@
{
+ "location-column-header": "Konfig.-Variablen",
"module_name": "Konfigurationsvariablen",
+ "overriden-vars-for-location": "F\u00fcr diesen Ort \u00fcberschriebene Variablen",
+ "overriden-vars-machines": "Rechner an diesem Ort, die \u00fcberschriebene Variablen haben",
"source-default": "Auslieferungszustand",
"source-global": "Global"
} \ No newline at end of file
diff --git a/modules-available/baseconfig/lang/en/module.json b/modules-available/baseconfig/lang/en/module.json
index 97e19f92..5a25bcff 100644
--- a/modules-available/baseconfig/lang/en/module.json
+++ b/modules-available/baseconfig/lang/en/module.json
@@ -1,5 +1,8 @@
{
+ "location-column-header": "Config. Vars",
"module_name": "Config Variables",
+ "overriden-vars-for-location": "Number of variables overridden at this location",
+ "overriden-vars-machines": "Machines at this location that have overridden variables",
"source-default": "Factory default",
"source-global": "Global"
} \ No newline at end of file
diff --git a/modules-available/baseconfig_bwlp/lang/de/config-variables.json b/modules-available/baseconfig_bwlp/lang/de/config-variables.json
index 1a043d1d..8270a7c3 100644
--- a/modules-available/baseconfig_bwlp/lang/de/config-variables.json
+++ b/modules-available/baseconfig_bwlp/lang/de/config-variables.json
@@ -7,7 +7,7 @@
"SLX_DEMO_PASS": "Passwort f\u00fcr den eingebauten *demo*-Account. Leer lassen, um das Einloggen zu verbieten.\r\n\/Hinweis\/: Das Passwort wird im Klartext in der lokalen Datenbank hinterlegt, jedoch immer gehasht an die Clients \u00fcbermittelt (SHA-512 mit Salt). Wenn Sie das Passwort auch im Satelliten nicht im Klartext speichern wollen, k\u00f6nnen Sie hier auch ein vorgehashtes Passwort eintragen (im *$6$....*-Format).",
"SLX_FORCE_RESOLUTION": "Wenn gesetzt, wird unabh\u00e4ngig von ermittelten Bildschirmdaten immer diese Aufl\u00f6sung konfiguriert.\r\nWenn Sie hier eine mit dem verbundenen Bildschirm inkompatible Ausl\u00f6sung setzen, bleibt mitunter der Bildschirm schwarz.",
"SLX_JUMBO_FRAMES": "Setzt die MTU auf den Clients auf 9000, statt wie \u00fcblich 1500. Da dies mit alten\/schlechten Routern oder Switches zu Problemen f\u00fchren k\u00f6nnte, ist diese Option standardm\u00e4\u00dfig deaktiviert.",
- "SLX_LOGOUT_TIMEOUT": "Zeit in Sekunden, die eine Benutzersitzung ohne Aktion sein darf, bevor sie beendet wird.Feld leer lassen, um die Funktion zu deaktivieren.",
+ "SLX_LOGOUT_TIMEOUT": "Zeit in Sekunden, die eine Benutzersitzung ohne Aktion sein darf, bevor sie beendet wird. Feld leer lassen, um die Funktion zu deaktivieren.",
"SLX_NET_DOMAIN": "DNS-Dom\u00e4ne, in die sich die Clients eingliedern, sofern der DHCP Server keine solche vorgibt.",
"SLX_NET_SEARCH": "Per Leerzeichen getrennte Liste von Suchdom\u00e4nen, die der Client verwenden soll, sofern der DHCP-Server keine Vorgabe macht.",
"SLX_NTFSFREE": "Bestimmt, ob freier Speicherplatz auf NTFS-Partitionen als tempor\u00e4rer Speicher, \u00e4quivalent zu einer ID44-Partition, genutzt werden soll.\r\n\r\n*never* deaktiviert diese Funktion.\r\n*backup* verwendet zun\u00e4chst eine ID44 Partition oder RAM-Disk, wenn keine Partition vorhanden ist, und weicht erst auf eine eventuell vorhandene NTFS-Partition aus, wenn der Speicher knapp wird.\r\n*always* verwendet immer eine vorhandene NTFS-Partition als tempor\u00e4ren Speicher, au\u00dfer es wurde eine ID44-Partition gefunden, die >= 100GiB ist.\r\n\r\nDiese Funktionalit\u00e4t steht nur bei Verwendung des MaxiLinux zur Verf\u00fcgung. Eine NTFS-Partition kann nur dann verwendet werden, wenn sie zuvor sauber ausgeh\u00e4ngt wurde, d.h. i.d.R., dass Windows ordnungsgem\u00e4\u00df heruntergefahren wurde.",
diff --git a/modules-available/baseconfig_bwlp/lang/en/config-variables.json b/modules-available/baseconfig_bwlp/lang/en/config-variables.json
index 7427bb17..9398747d 100644
--- a/modules-available/baseconfig_bwlp/lang/en/config-variables.json
+++ b/modules-available/baseconfig_bwlp/lang/en/config-variables.json
@@ -7,7 +7,7 @@
"SLX_DEMO_PASS": "Password for the *demo* account. Leave empty to disallow logging in as the demo user.\r\n\/Hint\/: The password SHA-512-with-salt hashed before it's being sent to the client. It's only stored in clear text on the Satellite Server. If you want to have it hashed on the server too, you can supply a pre-hashed password in \/$6$...$...\/-format.",
"SLX_FORCE_RESOLUTION": "If set, this resolution will be configured on the client regardless of what the connected screen(s) say they're capable of.\r\n\r\nIf you set this to something the connected screen is not compatible with, you might end up with a blank screen.",
"SLX_JUMBO_FRAMES": "Increases the MTU on the clients from 1500 to 9000. As this can lead to issues with old\/bad routers and switches, this option is disabled by default.",
- "SLX_LOGOUT_TIMEOUT": "Time in seconds, in which a user session may remain without action before it is terminated.Leave field blank to disable the function.",
+ "SLX_LOGOUT_TIMEOUT": "Time in seconds, in which a user session may remain without action before it is terminated. Leave field blank to disable the function.",
"SLX_NET_DOMAIN": "DNS domain in which the client integrate, provided the DHCP server does not specifies such.",
"SLX_NET_SEARCH": "Space separated list of DNS search domains to use in case the DHCP server doesn't supply any.",
"SLX_NTFSFREE": "Set whether free space on NTFS partitions will be used as temporary storage, just like an ID44 partition would.\r\n\r\n*never* disables this feature.\r\n*backup* only uses that space if the regular ID44 partition or RAM disk runs out of space.\r\n*always* will immediately make use of NTFS partitions, unless there is a large (>= 100GiB) ID44 partition.\r\n\r\nThis feature is only available when using MaxiLinux, and it only works if the NTFS partition has been unmounted cleanly before, i.e. Windows has been shut down properly.",
diff --git a/modules-available/locations/inc/abstractlocationcolumn.inc.php b/modules-available/locations/inc/abstractlocationcolumn.inc.php
new file mode 100644
index 00000000..9583429e
--- /dev/null
+++ b/modules-available/locations/inc/abstractlocationcolumn.inc.php
@@ -0,0 +1,24 @@
+<?php
+
+abstract class AbstractLocationColumn
+{
+
+ public abstract function getColumnHtml(int $locationId): string;
+
+ public abstract function getEditUrl(int $locationId): string;
+
+ public abstract function header(): string;
+
+ public abstract function priority(): int;
+
+ public function propagateColumn(): bool
+ {
+ return false;
+ }
+
+ public function propagateDefaultHtml(): string
+ {
+ return $this->getColumnHtml(0);
+ }
+
+} \ No newline at end of file
diff --git a/modules-available/locations/pages/locations.inc.php b/modules-available/locations/pages/locations.inc.php
index ce08e597..0eb035c8 100644
--- a/modules-available/locations/pages/locations.inc.php
+++ b/modules-available/locations/pages/locations.inc.php
@@ -93,126 +93,43 @@ class SubPage
$unassignedIdle = $unassignedLoad = $unassignedOverrides = 0;
$allowedLocationIds = User::getAllowedLocations("location.view");
- foreach (array_keys($locationList) as $lid) {
- if (!User::hasPermission('.baseconfig.view', $lid)) {
- $locationList[$lid]['havebaseconfig'] = false;
- }
- if (!User::hasPermission('.sysconfig.config.view-list', $lid)) {
- $locationList[$lid]['havesysconfig'] = false;
- }
- if (!User::hasPermission('.statistics.view.list', $lid)) {
- $locationList[$lid]['havestatistics'] = false;
- }
- if (!User::hasPermission('.serversetup.ipxe.menu.assign', $lid)) {
- $locationList[$lid]['haveipxe'] = false;
- }
- if (!in_array($lid, $allowedLocationIds)) {
- $locationList[$lid]['show-only'] = true;
- }
- }
- // Client statistics
- if (Module::get('statistics') !== false) {
- $unassigned = 0;
- $extra = '';
- if (in_array(0, $allowedLocationIds)) {
- $extra = ' OR locationid IS NULL';
- }
- $res = Database::simpleQuery("SELECT m.locationid, Count(*) AS cnt,
- Sum(If(m.state = 'OCCUPIED', 1, 0)) AS used, Sum(If(m.state = 'IDLE', 1, 0)) AS idle
- FROM machine m WHERE (locationid IN (:allowedLocationIds) $extra) GROUP BY locationid", compact('allowedLocationIds'));
- foreach ($res as $row) {
- $locId = (int)$row['locationid'];
- if (isset($locationList[$locId])) {
- $locationList[$locId]['clientCount'] = $row['cnt'];
- $locationList[$locId]['clientLoad'] = round(100 * $row['used'] / $row['cnt']);
- $locationList[$locId]['clientIdle'] = round(100 * ($row['used'] + $row['idle']) / $row['cnt']);
- } else {
- $unassigned += $row['cnt'];
- $unassignedLoad += $row['used'];
- $unassignedIdle += $row['idle'];
- }
- }
- $res = Database::simpleQuery("SELECT m.locationid, Count(DISTINCT sm.machineuuid) AS cnt FROM setting_machine sm
- INNER JOIN machine m USING (machineuuid) GROUP BY m.locationid");
- foreach ($res as $row) {
- $locId = (int)$row['locationid'];
- if (isset($locationList[$locId])) {
- $locationList[$locId]['machineVarsOverrideCount'] = $row['cnt'];
- } else {
- $unassignedOverrides += $row['cnt'];
- }
- }
- unset($loc);
- foreach ($locationList as &$loc) {
- if (!in_array($loc['locationid'], $allowedLocationIds))
- continue;
- if (!isset($loc['clientCountSum'])) {
- $loc['clientCountSum'] = 0;
- }
- if (!isset($loc['clientCount'])) {
- $loc['clientCount'] = $loc['clientIdle'] = $loc['clientLoad'] = 0;
- }
- $loc['clientCountSum'] += $loc['clientCount'];
- foreach ($loc['parents'] as $pid) {
- if (!in_array($pid, $allowedLocationIds))
- continue;
- $locationList[(int)$pid]['hasChild'] = true;
- $locationList[(int)$pid]['clientCountSum'] += $loc['clientCount'];
- }
- }
- unset($loc);
-
- }
- // Show currently active sysconfig for each location
- $defaultConfig = false;
- if (Module::isAvailable('sysconfig')) {
- $confs = SysConfig::getAll();
- foreach ($confs as $conf) {
- if (strlen($conf['locs']) === 0)
- continue;
- $confLocs = explode(',', $conf['locs']);
- foreach ($confLocs as $locId) {
- settype($locId, 'int');
- if ($locId === 0) {
- $defaultConfig = $conf['title'];
+ $plugins = [];
+ foreach (Hook::load('locations-column') as $hook) {
+ $c = @include($hook->file);
+ if ($c instanceof AbstractLocationColumn) {
+ $plugins[sprintf('%04d.%s', $c->priority(), $hook->moduleId)] = $c;
+ } elseif (is_array($c)) {
+ foreach ($c as $i => $cc) {
+ if ($cc instanceof AbstractLocationColumn) {
+ $plugins[sprintf('%04d.%d.%s', $cc->priority(), $i, $hook->moduleId)] = $cc;
}
- if (!isset($locationList[$locId]))
- continue;
- $locationList[$locId] += array('configName' => $conf['title'], 'configClass' => 'slx-bold');
}
}
- self::propagateFields($locationList, $defaultConfig, 'configName', 'configClass');
}
- // Count overridden config vars
- if (Module::get('baseconfig') !== false) {
- $res = Database::simpleQuery("SELECT locationid, Count(*) AS cnt FROM `setting_location`
- WHERE locationid IN (:allowedLocationIds) GROUP BY locationid", compact('allowedLocationIds'));
- foreach ($res as $row) {
- $lid = (int)$row['locationid'];
- if (isset($locationList[$lid])) {
- $locationList[$lid]['overriddenVars'] = $row['cnt'];
- }
+ ksort($plugins);
+ foreach ($locationList as $lid => &$loc) {
+ $loc['plugins'] = [];
+ foreach ($plugins as $pk => $plugin) {
+ $loc['plugins'][$pk] = [
+ 'url' => $plugin->getEditUrl($lid),
+ 'html' => $plugin->getColumnHtml($lid),
+ ];
+ }
+ if (!in_array($lid, $allowedLocationIds)) {
+ $locationList[$lid]['show-only'] = true;
}
- // Confusing because the count might be inaccurate within a branch
- //$this->propagateFields($locationList, '', 'overriddenVars', 'overriddenClass');
}
- // Show ipxe menu
- if (Module::isAvailable('serversetup') && class_exists('IPxe')) {
- $res = Database::simpleQuery("SELECT ml.locationid, m.title, ml.defaultentryid FROM serversetup_menu m
- INNER JOIN serversetup_menu_location ml USING (menuid)
- WHERE locationid IN (:allowedLocationIds) GROUP BY locationid", compact('allowedLocationIds'));
- foreach ($res as $row) {
- $lid = (int)$row['locationid'];
- if (isset($locationList[$lid])) {
- if ($row['defaultentryid'] !== null) {
- $row['title'] .= '(*)';
- }
- $locationList[$lid]['customMenu'] = $row['title'];
- }
+ unset($loc);
+ foreach ($plugins as $pk => $plugin) {
+ if ($plugin->propagateColumn()) {
+ self::propagateFields($locationList, $plugin->propagateDefaultHtml(), $pk);
}
- self::propagateFields($locationList, '', 'customMenu', 'customMenuClass');
}
+ foreach ($locationList as &$loc) {
+ $loc['plugins'] = array_values($loc['plugins']);
+ }
+ unset($loc);
$addAllowedLocs = User::getAllowedLocations("location.add");
$addAllowedList = Location::getLocations(0, 0, true);
@@ -224,44 +141,37 @@ class SubPage
unset($loc);
// Output
- $data = array(
+ $data = [
+ 'colspan' => (2 + count($plugins)),
+ 'plugins' => array_values($plugins),
'list' => array_values($locationList),
- 'havestatistics' => Module::get('statistics') !== false,
- 'havebaseconfig' => Module::get('baseconfig') !== false,
- 'havesysconfig' => Module::get('sysconfig') !== false,
- 'haveipxe' => Module::isAvailable('serversetup') && class_exists('IPxe'),
'overlapSelf' => $overlapSelf,
'overlapOther' => $overlapOther,
'mismatchMachines' => $mismatchMachines,
- 'unassignedCount' => $unassigned,
- 'unassignedLoad' => ($unassigned ? (round(($unassignedLoad / $unassigned) * 100)) : ''),
- 'unassignedIdle' => ($unassigned ? (round((($unassignedLoad + $unassignedIdle) / $unassigned) * 100)) : ''),
- 'unassignedOverrides' => $unassignedOverrides,
- 'defaultConfig' => $defaultConfig,
'addAllowedList' => array_values($addAllowedList),
- );
- // TODO: Buttons for config vars and sysconfig are currently always shown, as their availability
- // depends on permissions in the according modules, not this one
+ ];
Permission::addGlobalTags($data['perms'], NULL, ['subnets.edit', 'location.add']);
Render::addTemplate('locations', $data);
Module::isAvailable('js_ip'); // For CIDR magic
}
- private static function propagateFields(&$locationList, $defaultValue, $name, $class)
+ private static function propagateFields(array &$locationList, string $defaultValue, string $plugin)
{
$depth = array();
foreach ($locationList as &$loc) {
$d = $loc['depth'];
- if (!isset($loc[$name])) {
+ if (empty($loc['plugins'][$plugin]['html'])) {
// Has no explicit config assignment
if ($d === 0) {
- $loc[$name] = $defaultValue;
+ $loc['plugins'][$plugin]['html'] = $defaultValue;
} else {
- $loc[$name] = $depth[$d - 1];
+ $loc['plugins'][$plugin]['html'] = $depth[$d - 1];
}
- $loc[$class] = 'gray';
+ $loc['plugins'][$plugin]['class'] = 'gray';
+ } elseif (empty($loc['plugins'][$plugin]['class'])) {
+ $loc['plugins'][$plugin]['class'] = 'slx-bold';
}
- $depth[$d] = $loc[$name];
+ $depth[$d] = $loc['plugins'][$plugin]['html'];
unset($depth[$d + 1]);
}
}
diff --git a/modules-available/locations/style.css b/modules-available/locations/style.css
index 19950a38..0de0a801 100644
--- a/modules-available/locations/style.css
+++ b/modules-available/locations/style.css
@@ -1,4 +1,4 @@
-table.locations tbody td:nth-of-type(even) {
+table.locations > tbody > tr > td:nth-of-type(even) {
background-color: rgba(0, 0, 0, 0.025);
}
@@ -16,4 +16,12 @@ table.locations tbody td:nth-of-type(even) {
.load-col {
text-align: right;
text-shadow: 1px 1px #fff;
-} \ No newline at end of file
+ margin:0 -5px;
+ min-width: 80px;
+}
+
+.edit-btn {
+ background: inherit;
+ padding:0 2px;
+ text-align: right;
+}
diff --git a/modules-available/locations/templates/locations.html b/modules-available/locations/templates/locations.html
index 125c101a..efd48216 100644
--- a/modules-available/locations/templates/locations.html
+++ b/modules-available/locations/templates/locations.html
@@ -31,21 +31,11 @@
<table class="table table-condensed table-hover locations" style="margin-bottom:0">
<tr>
<th width="100%">{{lang_locationName}}</th>
- <th>
- {{#havestatistics}}{{lang_machineCount}}{{/havestatistics}}
- </th>
- <th>
- {{#havestatistics}}{{lang_machineLoad}}{{/havestatistics}}
- </th>
- <th class="text-nowrap">
- {{#havebaseconfig}}{{lang_editConfigVariables}}{{/havebaseconfig}}
- </th>
+ {{#plugins}}
<th class="text-nowrap">
- {{#havesysconfig}}{{lang_sysConfig}}{{/havesysconfig}}
- </th>
- <th class="text-nowrap">
- {{#haveipxe}}{{lang_bootMenu}}{{/haveipxe}}
+ {{header}}
</th>
+ {{/plugins}}
</tr>
{{#list}}
<tr>
@@ -61,85 +51,30 @@
</a>
{{/show-only}}
</td>
- <td class="text-nowrap" align="right">
- {{#havestatistics}}
- <a href="?do=Statistics&amp;show=list&amp;filters=location={{locationid}}">&nbsp;{{clientCount}}&nbsp;</a>
- <span style="display:inline-block;width:5ex">
- {{#hasChild}}
- (<a href="?do=Statistics&amp;show=list&amp;filters=location~{{locationid}}">&downarrow;{{clientCountSum}}</a>)
- {{/hasChild}}
- </span>
- {{/havestatistics}}
- </td>
- <td class="text-nowrap load-col" {{#clientCount}} style="background:linear-gradient(to right, #f97, #f97 {{clientLoad}}%, #6fa {{clientLoad}}%, #6fa {{clientIdle}}%, #eee {{clientIdle}}%)"{{/clientCount}}>
- {{#clientCount}}
- {{clientLoad}}&thinsp;%
- {{/clientCount}}
- </td>
- <td class="text-nowrap {{overriddenClass}}">
- {{#havebaseconfig}}
- <div class="pull-right" style="z-index:-1">
- <a class="btn btn-default btn-xs" href="?do=baseconfig&amp;module=locations&amp;locationid={{locationid}}"><span class="glyphicon glyphicon-edit"></span></a>
- </div>
- {{#overriddenVars}}
- <span class="badge" title="{{lang_overridenVarsForLocation}}">
- <span class="glyphicon glyphicon-home"></span> {{.}}
- </span>
- {{/overriddenVars}}
- {{#machineVarsOverrideCount}}
- <span class="badge" title="{{lang_numMachinesWithOverrides}}">
- <span class="glyphicon glyphicon-tasks"></span> {{.}}
- </span>
- {{/machineVarsOverrideCount}}
- &emsp;&emsp;
- {{/havebaseconfig}}
- </td>
- <td class="text-nowrap">
- {{#havesysconfig}}
- <div class="pull-right">
- <a class="btn btn-default btn-xs" href="?do=sysconfig&amp;locationid={{locationid}}"><span class="glyphicon glyphicon-edit"></span></a>
- </div>
- <span class="{{configClass}}">
- {{configName}}&emsp;&emsp;
- </span>
- {{/havesysconfig}}
- </td>
- <td class="text-nowrap">
- {{#haveipxe}}
- <div class="pull-right">
- <a class="btn btn-default btn-xs" href="?do=serversetup&amp;show=assignlocation&amp;locationid={{locationid}}"><span class="glyphicon glyphicon-edit"></span></a>
- </div>
- <span class="{{customMenuClass}}">
- {{customMenu}}&emsp;&emsp;
- </span>
- {{/haveipxe}}
+ {{#plugins}}
+ <td>
+ <table width="100%"><tr>
+ <td class="text-nowrap {{class}}">{{{html}}}</td>
+ {{#url}}
+ <td class="edit-btn">
+ <a class="btn btn-default btn-xs" href="{{.}}">
+ <span class="glyphicon glyphicon-edit"></span>
+ </a>
+ </td>
+ {{/url}}
+ </tr></table>
</td>
+ {{/plugins}}
</tr>
{{/list}}
- {{#unassignedCount}}
<tr>
<td>{{lang_unassignedMachines}}</td>
- <td class="text-nowrap" align="right">
- <a href="?do=Statistics&amp;show=list&amp;filters=location=0">
- &nbsp;{{unassignedCount}}&nbsp;
- </a>
- <span style="display:inline-block;width:5ex"></span>
- </td>
- <td class="text-nowrap load-col"{{#unassignedCount}} style="background:linear-gradient(to right, #f97, #f97 {{unassignedLoad}}%, #6fa {{unassignedLoad}}%, #6fa {{unassignedIdle}}%, #eee {{unassignedIdle}}%)"{{/unassignedCount}}>
- {{#unassignedCount}}
- {{unassignedLoad}}&thinsp;%
- {{/unassignedCount}}
- </td>
- <td>
- {{#unassignedOverrides}}
- <span class="badge" title="{{lang_numMachinesWithOverrides}}">
- <span class="glyphicon glyphicon-tasks"></span> {{.}}
- </span>
- {{/unassignedOverrides}}
+ {{#plugins}}
+ <td class="text-nowrap">
+ {{{propagateDefaultHtml}}}
</td>
- <td>{{defaultConfig}}</td>
+ {{/plugins}}
</tr>
- {{/unassignedCount}}
</table>
<form method="post" action="?do=Locations">
<input type="hidden" name="token" value="{{token}}">
@@ -214,7 +149,7 @@ function slxOpenLocation(e, lid) {
}
return;
}
- var td = $('<td>').attr('colspan', '6').css('padding', '0px 0px 12px');
+ var td = $('<td>').attr('colspan', '{{colspan}}').css('padding', '0px 0px 12px');
var tr = $('<tr>').attr('id', 'location-details-' + lid);
tr.append(td);
$(e).closest('tr').addClass('active slx-bold').after(tr);
diff --git a/modules-available/serversetup-bwlp-ipxe/hooks/locations-column.inc.php b/modules-available/serversetup-bwlp-ipxe/hooks/locations-column.inc.php
new file mode 100644
index 00000000..d3290b62
--- /dev/null
+++ b/modules-available/serversetup-bwlp-ipxe/hooks/locations-column.inc.php
@@ -0,0 +1,57 @@
+<?php
+
+if (!User::hasPermission('.serversetup.ipxe.menu.assign')
+ || !Module::isAvailable('serversetup')
+ || !class_exists('IPxe')) {
+ return null;
+}
+
+class IpxeLocationColumn extends AbstractLocationColumn
+{
+
+ private $lookup = [];
+
+ public function __construct(array $allowedLocationIds)
+ {
+ $res = Database::simpleQuery("SELECT ml.locationid, m.title, ml.defaultentryid FROM serversetup_menu m
+ INNER JOIN serversetup_menu_location ml USING (menuid)
+ WHERE locationid IN (:allowedLocationIds) GROUP BY locationid", compact('allowedLocationIds'));
+ foreach ($res as $row) {
+ $lid = (int)$row['locationid'];
+ if ($row['defaultentryid'] !== null) {
+ $row['title'] .= '(*)';
+ }
+ $this->lookup[$lid] = $row['title'];
+ }
+ }
+
+ public function getColumnHtml(int $locationId): string
+ {
+ return htmlspecialchars($this->lookup[$locationId] ?? '');
+ }
+
+ public function getEditUrl(int $locationId): string
+ {
+ if (!User::hasPermission('.serversetup.ipxe.menu.assign', $locationId))
+ return '';
+ return '?do=serversetup&show=assignlocation&locationid=' . $locationId;
+ }
+
+ public function header(): string
+ {
+ return Dictionary::translateFileModule('serversetup', 'module', 'location-column-header');
+ }
+
+ public function priority(): int
+ {
+ return 3000;
+ }
+
+ public function propagateColumn(): bool
+ {
+ return true;
+ }
+
+}
+
+return new IpxeLocationColumn($allowedLocationIds); \ No newline at end of file
diff --git a/modules-available/serversetup-bwlp-ipxe/lang/de/module.json b/modules-available/serversetup-bwlp-ipxe/lang/de/module.json
index 43209a2e..559b84a5 100644
--- a/modules-available/serversetup-bwlp-ipxe/lang/de/module.json
+++ b/modules-available/serversetup-bwlp-ipxe/lang/de/module.json
@@ -9,6 +9,7 @@
"dl-usb": "USB-Image",
"dl-usbnic": "Mit USB Netzwerktreibern",
"dl-x86_64": "64\u2009Bit",
+ "location-column-header": "Bootmen\u00fc",
"module_name": "iPXE \/ Boot Menu",
"page_title": "PXE- und Boot-Einstellungen",
"submenu_address": "Boot-IP \/ iPXE-Version setzen",
diff --git a/modules-available/serversetup-bwlp-ipxe/lang/en/module.json b/modules-available/serversetup-bwlp-ipxe/lang/en/module.json
index 643e51ba..ce660fc1 100644
--- a/modules-available/serversetup-bwlp-ipxe/lang/en/module.json
+++ b/modules-available/serversetup-bwlp-ipxe/lang/en/module.json
@@ -9,6 +9,7 @@
"dl-usb": "thumb drive image",
"dl-usbnic": "with USB NIC drivers",
"dl-x86_64": "64 bit",
+ "location-column-header": "Boot menu",
"module_name": "iPXE \/ Boot Menu",
"page_title": "iPXE and boot settings",
"submenu_address": "Set boot IP \/ iPXE version",
diff --git a/modules-available/statistics/hooks/locations-column.inc.php b/modules-available/statistics/hooks/locations-column.inc.php
new file mode 100644
index 00000000..51f280be
--- /dev/null
+++ b/modules-available/statistics/hooks/locations-column.inc.php
@@ -0,0 +1,150 @@
+<?php
+
+if (!User::hasPermission('.statistics.view.list')) {
+ return null;
+}
+
+class ClientCountLocationColumn extends AbstractLocationColumn
+{
+
+ private $lookup;
+
+ public function __construct()
+ {
+ $this->lookup = StatisticsColumnGetData([]);
+ }
+
+ public function getColumnHtml(int $locationId): string
+ {
+ if (!isset($this->lookup[$locationId]))
+ return '';
+ if ($this->lookup[$locationId]['hasChild'] ?? false) {
+ $child = <<<EOF
+ (<a href="?do=Statistics&amp;show=list&amp;filters=location~{$locationId}">&downarrow;{$this->lookup[$locationId]['clientCountSum']}</a>)
+EOF;
+ } else {
+ $child = '';
+ }
+
+ return <<<EOF
+ <div class="pull-right">
+ <a href="?do=Statistics&amp;show=list&amp;filters=location={$locationId}">&nbsp;{$this->lookup[$locationId]['clientCount']}&nbsp;</a>
+ <span class="text-right" style="display:inline-block;width:6ex">$child</span>
+ </div>
+EOF;
+ }
+
+ public function getEditUrl(int $locationId): string
+ {
+ return '';
+ }
+
+ public function header(): string
+ {
+ return Dictionary::translateFileModule('statistics', 'module', 'location-column-header-count');
+ }
+
+ public function priority(): int
+ {
+ return 800;
+ }
+
+}
+
+class ClientLoadLocationColumn extends AbstractLocationColumn
+{
+
+ private $lookup;
+
+ public function __construct()
+ {
+ $this->lookup = StatisticsColumnGetData([]);
+ }
+
+ public function getColumnHtml(int $locationId): string
+ {
+ if (!isset($this->lookup[$locationId]) || $this->lookup[$locationId]['clientCount'] === 0)
+ return '';
+ $c =& $this->lookup[$locationId];
+ return <<<EOF
+ <div class="load-col text-right" style="background:linear-gradient(to right, #f97, #f97 {$c['clientLoad']}%,
+ #6fa {$c['clientLoad']}%, #6fa {$c['clientIdle']}%, #eee {$c['clientIdle']}%)">
+ {$c['clientLoad']}&thinsp;%
+ </div>
+EOF;
+
+ }
+
+ public function getEditUrl(int $locationId): string
+ {
+ return '';
+ }
+
+ public function header(): string
+ {
+ return Dictionary::translateFileModule('statistics', 'module', 'location-column-header-load');
+ }
+
+ public function priority(): int
+ {
+ return 900;
+ }
+
+}
+
+function StatisticsColumnGetData(array $allowedLocationIds): array
+{
+ static $data = [];
+ if (!empty($data))
+ return $data;
+ $extra = '';
+ if (in_array(0, $allowedLocationIds)) {
+ $extra = ' OR locationid IS NULL';
+ }
+ $locs = Location::getLocationsAssoc();
+ $res = Database::simpleQuery("SELECT m.locationid, Count(*) AS cnt,
+ Sum(If(m.state = 'OCCUPIED', 1, 0)) AS used, Sum(If(m.state = 'IDLE', 1, 0)) AS idle
+ FROM machine m WHERE (locationid IN (:allowedLocationIds) $extra) GROUP BY locationid", compact('allowedLocationIds'));
+ foreach ($res as $row) {
+ $locId = (int)$row['locationid'];
+ $data[$locId] = [
+ 'clientCount' => $row['cnt'],
+ 'clientLoad' => round(100 * $row['used'] / $row['cnt']),
+ 'clientIdle' => round(100 * ($row['used'] + $row['idle']) / $row['cnt']),
+ ];
+ }
+ foreach ($allowedLocationIds as $locId) {
+ if (isset($data[$locId]))
+ continue;
+ $data[$locId] = [
+ 'clientCount' => 0,
+ 'clientLoad' => 0,
+ 'clientIdle' => 0,
+ ];
+ }
+ foreach ($data as $locId => &$loc) {
+ if (!in_array($locId, $allowedLocationIds))
+ continue;
+ if (!isset($loc['clientCountSum'])) {
+ $loc['clientCountSum'] = 0;
+ }
+ $loc['clientCountSum'] += $loc['clientCount'];
+ if ($locId !== 0) {
+ foreach ($locs[$locId]['parents'] as $pid) {
+ if (!in_array($pid, $allowedLocationIds))
+ continue;
+ $data[$pid]['hasChild'] = true;
+ if (!isset($data[$pid]['clientCountSum'])) {
+ $data[$pid]['clientCountSum'] = 0;
+ }
+ $data[$pid]['clientCountSum'] += $loc['clientCount'];
+ }
+ }
+ }
+ unset($loc);
+ return $data;
+}
+
+StatisticsColumnGetData($allowedLocationIds);
+
+return [new ClientCountLocationColumn(), new ClientLoadLocationColumn()]; \ No newline at end of file
diff --git a/modules-available/statistics/lang/de/module.json b/modules-available/statistics/lang/de/module.json
index a3006881..23fc52df 100644
--- a/modules-available/statistics/lang/de/module.json
+++ b/modules-available/statistics/lang/de/module.json
@@ -1,4 +1,6 @@
{
+ "location-column-header-count": "Rechner",
+ "location-column-header-load": "Besetzt",
"module_name": "Client-Statistiken",
"page_title": "Client-Statistiken",
"submenu_hints": "Hinweise",
diff --git a/modules-available/statistics/lang/en/module.json b/modules-available/statistics/lang/en/module.json
index d923ce7b..81e24a10 100644
--- a/modules-available/statistics/lang/en/module.json
+++ b/modules-available/statistics/lang/en/module.json
@@ -1,4 +1,6 @@
{
+ "location-column-header-count": "Clients",
+ "location-column-header-load": "Used",
"module_name": "Client Statistics",
"unused": "Unused"
} \ No newline at end of file
diff --git a/modules-available/sysconfig/hooks/locations-column.inc.php b/modules-available/sysconfig/hooks/locations-column.inc.php
new file mode 100644
index 00000000..f60a852d
--- /dev/null
+++ b/modules-available/sysconfig/hooks/locations-column.inc.php
@@ -0,0 +1,57 @@
+<?php
+
+if (!User::hasPermission('.sysconfig.config.*') || !Module::isAvailable('sysconfig'))
+ return null;
+
+class SysconfigLocationColumn extends AbstractLocationColumn
+{
+
+ private $lookup = [];
+
+ public function __construct()
+ {
+ $confs = SysConfig::getAll();
+ foreach ($confs as $conf) {
+ if (strlen($conf['locs']) === 0)
+ continue;
+ $confLocs = explode(',', $conf['locs']);
+ foreach ($confLocs as $locId) {
+ $this->lookup[$locId] = $conf['title'];
+ }
+ }
+ }
+
+ public function getColumnHtml(int $locationId): string
+ {
+ return htmlspecialchars($this->lookup[$locationId] ?? '');
+ }
+
+ public function getEditUrl(int $locationId): string
+ {
+ if (!User::hasPermission('.sysconfig.config.assign', $locationId))
+ return '';
+ return '?do=sysconfig&locationid=' . $locationId;
+ }
+
+ public function header(): string
+ {
+ return Dictionary::translateFileModule('sysconfig', 'module', 'location-column-header');
+ }
+
+ public function priority(): int
+ {
+ return 2000;
+ }
+
+ public function propagateColumn(): bool
+ {
+ return true;
+ }
+
+ public function propagateDefaultHtml(): string
+ {
+ return htmlspecialchars($this->lookup[0] ?? '');
+ }
+}
+
+return new SysconfigLocationColumn(); \ No newline at end of file
diff --git a/modules-available/sysconfig/lang/de/module.json b/modules-available/sysconfig/lang/de/module.json
index 1dbb268b..4bc1642f 100644
--- a/modules-available/sysconfig/lang/de/module.json
+++ b/modules-available/sysconfig/lang/de/module.json
@@ -6,8 +6,12 @@
"lang_moduleAssign": "Modul zu Systemkonfigurationen zuweisen",
"lang_noModuleFromThisGroup": "(Kein Modul dieser Gruppe)",
"lang_unknwonTaskManager": "Unbekannter Taskmanager-Fehler",
+ "location-column-header": "Lokalisierung",
"module_name": "Lokalisierung + Integration",
"page_title": "Lokalisierung + Integration",
+ "saver_DescriptionIdleKill": "Ein Bildschirmschoner mit Timeout, nach dessen Ablauf alle Anwendungen ohne weitere Nachfragen geschlossen werden und der Nutzer ausgeloggt wird.",
+ "saver_DescriptionNoTimeout": "Ein Bildschirmschoner ohne Timeout.",
+ "saver_DescriptionShutdown": "Ein Bildschirmschoner mit Timeout, nach dessen Ablauf alle Anwendungen ohne weitere Nachfragen geschlossen werden und der PC heruntergefahren oder neugestartet wird.",
"saver_MessageDefaultIdleKill": "Diese Sitzung wird bei Inaktivit\u00e4t in %1 beendet.",
"saver_MessageDefaultIdleKillLocked": "Diese Sitzung wird in %1 beendet, wenn sie nicht entsperrt wird.",
"saver_MessageDefaultNoTimeout": "Dieser Bildschirm wird gerade geschont.",
@@ -15,13 +19,10 @@
"saver_MessageDefaultShutdown": "Achtung: Rechner wird in %1 heruntergefahren!",
"saver_MessageDefaultShutdownLocked": "Achtung: Rechner wird in %1 heruntergefahren!",
"saver_QssDefault": "#Saver {\r\n background: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1, stop:0 #443, stop:1 #000)\r\n}\r\n\r\nQLabel {\r\n color: #f64;\r\n}\r\n\r\n#lblClock {\r\n color: #999;\r\n font-size: 20pt;\r\n}\r\n\r\n#lblHeader {\r\n font-size: 20pt;\r\n}\r\n",
- "saver_DescriptionIdleKill": "Ein Bildschirmschoner mit Timeout, nach dessen Ablauf alle Anwendungen ohne weitere Nachfragen geschlossen werden und der Nutzer ausgeloggt wird.",
- "saver_DescriptionNoTimeout": "Ein Bildschirmschoner ohne Timeout.",
- "saver_DescriptionShutdown": "Ein Bildschirmschoner mit Timeout, nach dessen Ablauf alle Anwendungen ohne weitere Nachfragen geschlossen werden und der PC heruntergefahren oder neugestartet wird.",
- "saver_TitleIdleKill": "Idle Kill",
- "saver_TitleNoTimeout": "Ohne Timeout",
- "saver_TitleShutdown": "Herunterfahren",
"saver_TextDefaultIdleKill": "<html><body>Keine Nutzeraktivit\u00e4t festgestellt. <br>Zum oben angegebenen Zeitpunkt wird die aktuell laufende Sitzung beendet, wenn der Rechner nicht mehr verwendet wird. <br>Alle noch laufenden Programme <br>werden ohne Nachfrage geschlossen. Stellen Sie daher sicher, bis zum angegebenen Zeitpunkt <br>s\u00e4mtliche sich in Bearbeitung befindlichen Daten abzuspeichern. <br><br>Dies dient dazu zu vermeiden, dass ein Rechner stundenlang gesperrt wird und somit <br>anderen Nutzern nicht zur Verf\u00fcgung steht.<\/body><\/html>",
"saver_TextDefaultIdleKillLocked": "<html><body><br>Zum oben angegebenen Zeitpunkt wird die aktuell laufende Sitzung beendet, wenn sie zuvor nicht wieder entsperrt wird. <br>Alle noch laufenden Programme werden ohne Nachfrage geschlossen. <br>Stellen Sie daher sicher, bis zum angegebenen Zeitpunkt <br>s\u00e4mtliche sich in Bearbeitung befindlichen Daten abzuspeichern, bzw. die Sitzung wieder zu entsperren. <br><br>Dies dient dazu zu vermeiden, dass ein Rechner stundenlang gesperrt wird und somit<br>anderen Nutzern nicht zur Verf\u00fcgung steht.<\/body><\/html>",
- "saver_TextDefaultShutdown": "<html><body>Achtung: Zum oben angegebenen Zeitpunkt wird der Computer heruntergefahren bzw. neugestartet. <br>Alle noch laufenden Programme werden ohne Nachfrage beendet. Stellen Sie daher sicher, bis <br>zum angegebenen Zeitpunkt s\u00e4mtliche Daten abzuspeichern und die Sitzung zu verlassen.<\/body><\/html>"
-}
+ "saver_TextDefaultShutdown": "<html><body>Achtung: Zum oben angegebenen Zeitpunkt wird der Computer heruntergefahren bzw. neugestartet. <br>Alle noch laufenden Programme werden ohne Nachfrage beendet. Stellen Sie daher sicher, bis <br>zum angegebenen Zeitpunkt s\u00e4mtliche Daten abzuspeichern und die Sitzung zu verlassen.<\/body><\/html>",
+ "saver_TitleIdleKill": "Idle Kill",
+ "saver_TitleNoTimeout": "Ohne Timeout",
+ "saver_TitleShutdown": "Herunterfahren"
+} \ No newline at end of file
diff --git a/modules-available/sysconfig/lang/en/module.json b/modules-available/sysconfig/lang/en/module.json
index b49cc1cf..dbfacdb8 100644
--- a/modules-available/sysconfig/lang/en/module.json
+++ b/modules-available/sysconfig/lang/en/module.json
@@ -6,8 +6,12 @@
"lang_moduleAssign": "Assign Module to System Configurations",
"lang_noModuleFromThisGroup": "(No module from this group)",
"lang_unknwonTaskManager": "Unknown Task Manager error",
+ "location-column-header": "SysConfig",
"module_name": "Localization",
"page_title": "Localize and integrate",
+ "saver_DescriptionIdleKill": "A screensaver with a timeout which on it's expiration will close all running applications without further requests and logout the user.",
+ "saver_DescriptionNoTimeout": "A screensaver without a timeout.",
+ "saver_DescriptionShutdown": "A screensaver with a timeout which on it's expiration the PC will shutdown or restart. All applications will be closed without further requests.",
"saver_MessageDefaultIdleKill": "This session will end in %1 when inactive.",
"saver_MessageDefaultIdleKillLocked": "This session will end in %1 if the session is not unlocked.",
"saver_MessageDefaultNoTimeout": "This screen is in saving mode.",
@@ -15,13 +19,10 @@
"saver_MessageDefaultShutdown": "Caution: Computer will shutdown in %1!",
"saver_MessageDefaultShutdownLocked": "Caution: Computer will shutdown in %1!",
"saver_QssDefault": "#Saver {\r\n background: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1, stop:0 #443, stop:1 #000)\r\n}\r\n\r\nQLabel {\r\n color: #f64;\r\n}\r\n\r\n#lblClock {\r\n color: #999;\r\n font-size: 20pt;\r\n}\r\n\r\n#lblHeader {\r\n font-size: 20pt;\r\n}\r\n",
- "saver_DescriptionIdleKill": "A screensaver with a timeout which on it's expiration will close all running applications without further requests and logout the user.",
- "saver_DescriptionNoTimeout": "A screensaver without a timeout.",
- "saver_DescriptionShutdown": "A screensaver with a timeout which on it's expiration the PC will shutdown or restart. All applications will be closed without further requests.",
- "saver_TitleIdleKill": "Idle Kill",
- "saver_TitleNoTimeout": "No Timeout",
- "saver_TitleShutdown": "Shutdown",
"saver_TextDefaultIdleKill": "<html><body>No user activity detected. <br>If the computer is not used until the time specified above, the session will end. <br> All running applications <br>will be closed without further requests. Make sure that all files and changes are saved <br> before the time runs out. <br><br>It prevents computers from beeing locked for hours and <br>not beeing available for other users.<\/body><\/html>",
"saver_TextDefaultIdleKillLocked": "<html><body><br>The current session will end by the time specified above if the computer isn't unlocked before. <br>All running applications will be closed without further requests. <br>Make sure that all files and changes are saved <br>or the session is unlocked before the time runs out. <br><br>It prevents computers from beeing locked for hours and <br>not beeing available for other users.<\/body><\/html>",
- "saver_TextDefaultShutdown": "<html><body>Caution: The computer will shutdown or restart respectively at the specified time above. <br>All running applications will be closed without further requests. Make sure to save all files and changes and leave the session<br>before the time runs out.<\/body><\/html>"
-}
+ "saver_TextDefaultShutdown": "<html><body>Caution: The computer will shutdown or restart respectively at the specified time above. <br>All running applications will be closed without further requests. Make sure to save all files and changes and leave the session<br>before the time runs out.<\/body><\/html>",
+ "saver_TitleIdleKill": "Idle Kill",
+ "saver_TitleNoTimeout": "No Timeout",
+ "saver_TitleShutdown": "Shutdown"
+} \ No newline at end of file