chg: [settings] Refactored settings table and views
Allow for improved re-usability to use the views and functions with other settingspull/72/head
parent
14c7d20cc1
commit
a2e3ad76dd
|
@ -1,28 +1,28 @@
|
||||||
<?php
|
<?php
|
||||||
namespace App\Model\Table;
|
namespace App\Settings\SettingsProvider;
|
||||||
|
|
||||||
use App\Model\Table\AppTable;
|
use App\Model\Table\AppTable;
|
||||||
use Cake\Validation\Validator;
|
use Cake\Validation\Validator;
|
||||||
use Cake\ORM\TableRegistry;
|
use Cake\ORM\TableRegistry;
|
||||||
|
|
||||||
class SettingsProviderTable extends AppTable
|
class BaseSettingsProvider
|
||||||
{
|
{
|
||||||
private $settingsConfiguration = [];
|
protected $settingsConfiguration = [];
|
||||||
private $error_critical = '',
|
protected $error_critical = '',
|
||||||
$error_warning = '',
|
$error_warning = '',
|
||||||
$error_info = '';
|
$error_info = '';
|
||||||
private $severities = ['info', 'warning', 'critical'];
|
protected $severities = ['info', 'warning', 'critical'];
|
||||||
|
|
||||||
public function initialize(array $config): void
|
public function __construct()
|
||||||
{
|
{
|
||||||
parent::initialize($config);
|
|
||||||
$this->settingsConfiguration = $this->generateSettingsConfiguration();
|
$this->settingsConfiguration = $this->generateSettingsConfiguration();
|
||||||
$this->setTable(false);
|
|
||||||
$this->error_critical = __('Cerebrate will not operate correctly or will be unsecure until these issues are resolved.');
|
$this->error_critical = __('Cerebrate will not operate correctly or will be unsecure until these issues are resolved.');
|
||||||
$this->error_warning = __('Some of the features of Cerebrate cannot be utilised until these issues are resolved.');
|
$this->error_warning = __('Some of the features of Cerebrate cannot be utilised until these issues are resolved.');
|
||||||
$this->error_info = __('There are some optional tweaks that could be done to improve the looks of your Cerebrate instance.');
|
$this->error_info = __('There are some optional tweaks that could be done to improve the looks of your Cerebrate instance.');
|
||||||
|
if (!isset($this->settingValidator)) {
|
||||||
$this->settingValidator = new SettingValidator();
|
$this->settingValidator = new SettingValidator();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Supports up to 3 levels:
|
* Supports up to 3 levels:
|
||||||
|
@ -46,151 +46,9 @@ class SettingsProviderTable extends AppTable
|
||||||
* redacted [optional]: Should the setting value be redacted. FIXME: To implement
|
* redacted [optional]: Should the setting value be redacted. FIXME: To implement
|
||||||
* cli_only [optional]: Should this setting be modified only via the CLI.
|
* cli_only [optional]: Should this setting be modified only via the CLI.
|
||||||
*/
|
*/
|
||||||
private function generateSettingsConfiguration()
|
protected function generateSettingsConfiguration()
|
||||||
{
|
{
|
||||||
return [
|
return [];
|
||||||
'Application' => [
|
|
||||||
'General' => [
|
|
||||||
'Essentials' => [
|
|
||||||
'_description' => __('Ensentials settings required for the application to run normally.'),
|
|
||||||
'_icon' => 'user-cog',
|
|
||||||
'app.baseurl' => [
|
|
||||||
'name' => __('Base URL'),
|
|
||||||
'type' => 'string',
|
|
||||||
'description' => __('The base url of the application (in the format https://www.mymispinstance.com or https://myserver.com/misp). Several features depend on this setting being correctly set to function.'),
|
|
||||||
'default' => '',
|
|
||||||
'severity' => 'critical',
|
|
||||||
'test' => 'testBaseURL',
|
|
||||||
],
|
|
||||||
'app.uuid' => [
|
|
||||||
'name' => 'UUID',
|
|
||||||
'type' => 'string',
|
|
||||||
'description' => __('The Cerebrate instance UUID. This UUID is used to identify this instance.'),
|
|
||||||
'default' => '',
|
|
||||||
'severity' => 'critical',
|
|
||||||
'test' => 'testUuid',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'Miscellaneous' => [
|
|
||||||
'sc2.hero' => [
|
|
||||||
'description' => 'The true hero',
|
|
||||||
'default' => 'Sarah Kerrigan',
|
|
||||||
'name' => 'Hero',
|
|
||||||
'options' => [
|
|
||||||
'Jim Raynor' => 'Jim Raynor',
|
|
||||||
'Sarah Kerrigan' => 'Sarah Kerrigan',
|
|
||||||
'Artanis' => 'Artanis',
|
|
||||||
'Zeratul' => 'Zeratul',
|
|
||||||
],
|
|
||||||
'type' => 'select'
|
|
||||||
],
|
|
||||||
'sc2.antagonists' => [
|
|
||||||
'description' => 'The bad guys',
|
|
||||||
'default' => 'Amon',
|
|
||||||
'name' => 'Antagonists',
|
|
||||||
'options' => function($settingsProviders) {
|
|
||||||
return [
|
|
||||||
'Amon' => 'Amon',
|
|
||||||
'Sarah Kerrigan' => 'Sarah Kerrigan',
|
|
||||||
'Narud' => 'Narud',
|
|
||||||
];
|
|
||||||
},
|
|
||||||
'severity' => 'warning',
|
|
||||||
'type' => 'multi-select'
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'floating-setting' => [
|
|
||||||
'description' => 'floaringSetting',
|
|
||||||
// 'default' => 'A default value',
|
|
||||||
'name' => 'Uncategorized Setting',
|
|
||||||
// 'severity' => 'critical',
|
|
||||||
'severity' => 'warning',
|
|
||||||
// 'severity' => 'info',
|
|
||||||
'type' => 'integer'
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'Network' => [
|
|
||||||
'Proxy' => [
|
|
||||||
'proxy.host' => [
|
|
||||||
'name' => __('Host'),
|
|
||||||
'type' => 'string',
|
|
||||||
'description' => __('The hostname of an HTTP proxy for outgoing sync requests. Leave empty to not use a proxy.'),
|
|
||||||
'test' => 'testHostname',
|
|
||||||
],
|
|
||||||
'proxy.port' => [
|
|
||||||
'name' => __('Port'),
|
|
||||||
'type' => 'integer',
|
|
||||||
'description' => __('The TCP port for the HTTP proxy.'),
|
|
||||||
'test' => 'testForRangeXY',
|
|
||||||
],
|
|
||||||
'proxy.user' => [
|
|
||||||
'name' => __('User'),
|
|
||||||
'type' => 'string',
|
|
||||||
'description' => __('The authentication username for the HTTP proxy.'),
|
|
||||||
'default' => 'admin',
|
|
||||||
'dependsOn' => 'proxy.host',
|
|
||||||
],
|
|
||||||
'proxy.password' => [
|
|
||||||
'name' => __('Password'),
|
|
||||||
'type' => 'string',
|
|
||||||
'description' => __('The authentication password for the HTTP proxy.'),
|
|
||||||
'default' => '',
|
|
||||||
'dependsOn' => 'proxy.host',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'UI' => [
|
|
||||||
'General' => [
|
|
||||||
'ui.bsTheme' => [
|
|
||||||
'description' => 'The Bootstrap theme to use for the application',
|
|
||||||
'default' => 'default',
|
|
||||||
'name' => 'UI Theme',
|
|
||||||
'options' => function($settingsProviders) {
|
|
||||||
$instanceTable = TableRegistry::getTableLocator()->get('Instance');
|
|
||||||
$themes = $instanceTable->getAvailableThemes();
|
|
||||||
return array_combine($themes, $themes);
|
|
||||||
},
|
|
||||||
'severity' => 'info',
|
|
||||||
'type' => 'select'
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'Security' => [
|
|
||||||
'Development' => [
|
|
||||||
'Debugging' => [
|
|
||||||
'security.debug' => [
|
|
||||||
'name' => __('Debug Level'),
|
|
||||||
'type' => 'select',
|
|
||||||
'description' => __('The debug level of the instance'),
|
|
||||||
'default' => 0,
|
|
||||||
'options' => [
|
|
||||||
0 => __('Debug Off'),
|
|
||||||
1 => __('Debug On'),
|
|
||||||
2 => __('Debug On + SQL Dump'),
|
|
||||||
],
|
|
||||||
'test' => function($value, $setting, $validator) {
|
|
||||||
$validator->range('value', [0, 3]);
|
|
||||||
return testValidator($value, $validator);
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'Features' => [
|
|
||||||
'Demo Settings' => [
|
|
||||||
'demo.switch' => [
|
|
||||||
'name' => __('Switch'),
|
|
||||||
'type' => 'boolean',
|
|
||||||
'description' => __('A switch acting as a checkbox'),
|
|
||||||
'default' => false,
|
|
||||||
'test' => function() {
|
|
||||||
return 'Fake error';
|
|
||||||
},
|
|
||||||
],
|
|
||||||
]
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -214,7 +72,7 @@ class SettingsProviderTable extends AppTable
|
||||||
* @param array $settings the settings
|
* @param array $settings the settings
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
private function mergeSettingsIntoSettingConfiguration(array $settingConf, array $settings, string $path=''): array
|
protected function mergeSettingsIntoSettingConfiguration(array $settingConf, array $settings, string $path=''): array
|
||||||
{
|
{
|
||||||
foreach ($settingConf as $key => $value) {
|
foreach ($settingConf as $key => $value) {
|
||||||
if ($this->isSettingMetaKey($key)) {
|
if ($this->isSettingMetaKey($key)) {
|
||||||
|
@ -277,12 +135,12 @@ class SettingsProviderTable extends AppTable
|
||||||
return $notices;
|
return $notices;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function isLeaf($setting)
|
protected function isLeaf($setting)
|
||||||
{
|
{
|
||||||
return !empty($setting['name']) && !empty($setting['type']);
|
return !empty($setting['name']) && !empty($setting['type']);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function evaluateLeaf($setting, $settingSection)
|
protected function evaluateLeaf($setting, $settingSection)
|
||||||
{
|
{
|
||||||
$skipValidation = false;
|
$skipValidation = false;
|
||||||
if ($setting['type'] == 'select' || $setting['type'] == 'multi-select') {
|
if ($setting['type'] == 'select' || $setting['type'] == 'multi-select') {
|
||||||
|
@ -353,12 +211,6 @@ class SettingsProviderTable extends AppTable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function testValidator($value, $validator)
|
|
||||||
{
|
|
||||||
$errors = $validator->validate(['value' => $value]);
|
|
||||||
return !empty($errors) ? implode(', ', $errors['value']) : true;
|
|
||||||
}
|
|
||||||
|
|
||||||
class SettingValidator
|
class SettingValidator
|
||||||
{
|
{
|
||||||
|
|
||||||
|
@ -384,22 +236,4 @@ class SettingValidator
|
||||||
{
|
{
|
||||||
return !empty($value) ? true : __('Cannot be empty');
|
return !empty($value) ? true : __('Cannot be empty');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testBaseURL($value, &$setting)
|
|
||||||
{
|
|
||||||
if (empty($value)) {
|
|
||||||
return __('Cannot be empty');
|
|
||||||
}
|
|
||||||
if (!empty($value) && !preg_match('/^http(s)?:\/\//i', $value)) {
|
|
||||||
return __('Invalid URL, please make sure that the protocol is set.');
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testUuid($value, &$setting) {
|
|
||||||
if (empty($value) || !preg_match('/^\{?[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}\}?$/', $value)) {
|
|
||||||
return __('Invalid UUID.');
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -0,0 +1,196 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Settings\SettingsProvider;
|
||||||
|
|
||||||
|
use Cake\ORM\TableRegistry;
|
||||||
|
|
||||||
|
require_once(APP . 'Model' . DS . 'Table' . DS . 'SettingProviders' . DS . 'BaseSettingsProvider.php');
|
||||||
|
|
||||||
|
use App\Settings\SettingsProvider\BaseSettingsProvider;
|
||||||
|
use App\Settings\SettingsProvider\SettingValidator;
|
||||||
|
|
||||||
|
class CerebrateSettingsProvider extends BaseSettingsProvider
|
||||||
|
{
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->settingValidator = new CerebrateSettingValidator();
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function generateSettingsConfiguration()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'Application' => [
|
||||||
|
'General' => [
|
||||||
|
'Essentials' => [
|
||||||
|
'_description' => __('Ensentials settings required for the application to run normally.'),
|
||||||
|
'_icon' => 'user-cog',
|
||||||
|
'app.baseurl' => [
|
||||||
|
'name' => __('Base URL'),
|
||||||
|
'type' => 'string',
|
||||||
|
'description' => __('The base url of the application (in the format https://www.mymispinstance.com or https://myserver.com/misp). Several features depend on this setting being correctly set to function.'),
|
||||||
|
'default' => '',
|
||||||
|
'severity' => 'critical',
|
||||||
|
'test' => 'testBaseURL',
|
||||||
|
],
|
||||||
|
'app.uuid' => [
|
||||||
|
'name' => 'UUID',
|
||||||
|
'type' => 'string',
|
||||||
|
'description' => __('The Cerebrate instance UUID. This UUID is used to identify this instance.'),
|
||||||
|
'default' => '',
|
||||||
|
'severity' => 'critical',
|
||||||
|
'test' => 'testUuid',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'Miscellaneous' => [
|
||||||
|
'sc2.hero' => [
|
||||||
|
'description' => 'The true hero',
|
||||||
|
'default' => 'Sarah Kerrigan',
|
||||||
|
'name' => 'Hero',
|
||||||
|
'options' => [
|
||||||
|
'Jim Raynor' => 'Jim Raynor',
|
||||||
|
'Sarah Kerrigan' => 'Sarah Kerrigan',
|
||||||
|
'Artanis' => 'Artanis',
|
||||||
|
'Zeratul' => 'Zeratul',
|
||||||
|
],
|
||||||
|
'type' => 'select'
|
||||||
|
],
|
||||||
|
'sc2.antagonists' => [
|
||||||
|
'description' => 'The bad guys',
|
||||||
|
'default' => 'Amon',
|
||||||
|
'name' => 'Antagonists',
|
||||||
|
'options' => function ($settingsProviders) {
|
||||||
|
return [
|
||||||
|
'Amon' => 'Amon',
|
||||||
|
'Sarah Kerrigan' => 'Sarah Kerrigan',
|
||||||
|
'Narud' => 'Narud',
|
||||||
|
];
|
||||||
|
},
|
||||||
|
'severity' => 'warning',
|
||||||
|
'type' => 'multi-select'
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'floating-setting' => [
|
||||||
|
'description' => 'floaringSetting',
|
||||||
|
// 'default' => 'A default value',
|
||||||
|
'name' => 'Uncategorized Setting',
|
||||||
|
// 'severity' => 'critical',
|
||||||
|
'severity' => 'warning',
|
||||||
|
// 'severity' => 'info',
|
||||||
|
'type' => 'integer'
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'Network' => [
|
||||||
|
'Proxy' => [
|
||||||
|
'proxy.host' => [
|
||||||
|
'name' => __('Host'),
|
||||||
|
'type' => 'string',
|
||||||
|
'description' => __('The hostname of an HTTP proxy for outgoing sync requests. Leave empty to not use a proxy.'),
|
||||||
|
'test' => 'testHostname',
|
||||||
|
],
|
||||||
|
'proxy.port' => [
|
||||||
|
'name' => __('Port'),
|
||||||
|
'type' => 'integer',
|
||||||
|
'description' => __('The TCP port for the HTTP proxy.'),
|
||||||
|
'test' => 'testForRangeXY',
|
||||||
|
],
|
||||||
|
'proxy.user' => [
|
||||||
|
'name' => __('User'),
|
||||||
|
'type' => 'string',
|
||||||
|
'description' => __('The authentication username for the HTTP proxy.'),
|
||||||
|
'default' => 'admin',
|
||||||
|
'dependsOn' => 'proxy.host',
|
||||||
|
],
|
||||||
|
'proxy.password' => [
|
||||||
|
'name' => __('Password'),
|
||||||
|
'type' => 'string',
|
||||||
|
'description' => __('The authentication password for the HTTP proxy.'),
|
||||||
|
'default' => '',
|
||||||
|
'dependsOn' => 'proxy.host',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'UI' => [
|
||||||
|
'General' => [
|
||||||
|
'ui.bsTheme' => [
|
||||||
|
'description' => 'The Bootstrap theme to use for the application',
|
||||||
|
'default' => 'default',
|
||||||
|
'name' => 'UI Theme',
|
||||||
|
'options' => function ($settingsProviders) {
|
||||||
|
$instanceTable = TableRegistry::getTableLocator()->get('Instance');
|
||||||
|
$themes = $instanceTable->getAvailableThemes();
|
||||||
|
return array_combine($themes, $themes);
|
||||||
|
},
|
||||||
|
'severity' => 'info',
|
||||||
|
'type' => 'select'
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'Security' => [
|
||||||
|
'Development' => [
|
||||||
|
'Debugging' => [
|
||||||
|
'security.debug' => [
|
||||||
|
'name' => __('Debug Level'),
|
||||||
|
'type' => 'select',
|
||||||
|
'description' => __('The debug level of the instance'),
|
||||||
|
'default' => 0,
|
||||||
|
'options' => [
|
||||||
|
0 => __('Debug Off'),
|
||||||
|
1 => __('Debug On'),
|
||||||
|
2 => __('Debug On + SQL Dump'),
|
||||||
|
],
|
||||||
|
'test' => function ($value, $setting, $validator) {
|
||||||
|
$validator->range('value', [0, 3]);
|
||||||
|
return testValidator($value, $validator);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'Features' => [
|
||||||
|
'Demo Settings' => [
|
||||||
|
'demo.switch' => [
|
||||||
|
'name' => __('Switch'),
|
||||||
|
'type' => 'boolean',
|
||||||
|
'description' => __('A switch acting as a checkbox'),
|
||||||
|
'default' => false,
|
||||||
|
'test' => function () {
|
||||||
|
return 'Fake error';
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testValidator($value, $validator)
|
||||||
|
{
|
||||||
|
$errors = $validator->validate(['value' => $value]);
|
||||||
|
return !empty($errors) ? implode(', ', $errors['value']) : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CerebrateSettingValidator extends SettingValidator
|
||||||
|
{
|
||||||
|
public function testUuid($value, &$setting)
|
||||||
|
{
|
||||||
|
if (empty($value) || !preg_match('/^\{?[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}\}?$/', $value)) {
|
||||||
|
return __('Invalid UUID.');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function testBaseURL($value, &$setting)
|
||||||
|
{
|
||||||
|
if (empty($value)) {
|
||||||
|
return __('Cannot be empty');
|
||||||
|
}
|
||||||
|
if (!empty($value) && !preg_match('/^http(s)?:\/\//i', $value)) {
|
||||||
|
return __('Invalid URL, please make sure that the protocol is set.');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,9 +3,10 @@ namespace App\Model\Table;
|
||||||
|
|
||||||
use App\Model\Table\AppTable;
|
use App\Model\Table\AppTable;
|
||||||
use Cake\ORM\Table;
|
use Cake\ORM\Table;
|
||||||
use Cake\Validation\Validator;
|
|
||||||
use Cake\Core\Configure;
|
use Cake\Core\Configure;
|
||||||
use Cake\ORM\TableRegistry;
|
|
||||||
|
require_once(APP . 'Model' . DS . 'Table' . DS . 'SettingProviders' . DS . 'CerebrateSettingsProvider.php');
|
||||||
|
use App\Settings\SettingsProvider\CerebrateSettingsProvider;
|
||||||
|
|
||||||
class SettingsTable extends AppTable
|
class SettingsTable extends AppTable
|
||||||
{
|
{
|
||||||
|
@ -16,7 +17,7 @@ class SettingsTable extends AppTable
|
||||||
{
|
{
|
||||||
parent::initialize($config);
|
parent::initialize($config);
|
||||||
$this->setTable(false);
|
$this->setTable(false);
|
||||||
$this->SettingsProvider = TableRegistry::getTableLocator()->get('SettingsProvider');
|
$this->SettingsProvider = new CerebrateSettingsProvider();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getSettings($full=false): array
|
public function getSettings($full=false): array
|
||||||
|
|
|
@ -26,8 +26,9 @@ array_unshift($tabContents, $notice);
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const variantFromSeverity = <?= json_encode($variantFromSeverity) ?>;
|
window.variantFromSeverity = <?= json_encode($variantFromSeverity) ?>;
|
||||||
const settingsFlattened = <?= json_encode($settingsFlattened) ?>;
|
window.settingsFlattened = <?= json_encode($settingsFlattened) ?>;
|
||||||
|
window.saveSettingURL = '/instance/saveSetting'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="px-5">
|
<div class="px-5">
|
||||||
|
@ -50,167 +51,23 @@ array_unshift($tabContents, $notice);
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
echo $this->Bootstrap->tabs($tabsOptions);
|
echo $this->Bootstrap->tabs($tabsOptions);
|
||||||
|
echo $this->Html->script('settings');
|
||||||
?>
|
?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
$(document).ready(function() {
|
|
||||||
new bootstrap.Tooltip('.depends-on-icon', {
|
|
||||||
placement: 'right',
|
|
||||||
})
|
|
||||||
$('select.custom-select[multiple]').select2()
|
|
||||||
|
|
||||||
$('.settings-tabs a[data-bs-toggle="tab"]').on('shown.bs.tab', function (event) {
|
|
||||||
$('[data-bs-spy="scroll"]').trigger('scroll.bs.scrollspy')
|
|
||||||
})
|
|
||||||
|
|
||||||
$('.tab-content input, .tab-content select').on('input', function() {
|
|
||||||
if ($(this).attr('type') == 'checkbox') {
|
|
||||||
const $input = $(this)
|
|
||||||
const $inputGroup = $(this).closest('.setting-group')
|
|
||||||
const settingName = $(this).data('setting-name')
|
|
||||||
const settingValue = $(this).is(':checked') ? 1 : 0
|
|
||||||
saveSetting($inputGroup[0], $input, settingName, settingValue)
|
|
||||||
} else {
|
|
||||||
handleSettingValueChange($(this))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
$('.tab-content .setting-group .btn-save-setting').click(function() {
|
|
||||||
const $input = $(this).closest('.input-group').find('input, select')
|
|
||||||
const settingName = $input.data('setting-name')
|
|
||||||
const settingValue = $input.val()
|
|
||||||
saveSetting(this, $input, settingName, settingValue)
|
|
||||||
})
|
|
||||||
$('.tab-content .setting-group .btn-reset-setting').click(function() {
|
|
||||||
const $btn = $(this)
|
|
||||||
const $input = $btn.closest('.input-group').find('input, select')
|
|
||||||
let oldValue = settingsFlattened[$input.data('setting-name')].value
|
|
||||||
if ($input.is('select')) {
|
|
||||||
oldValue = oldValue !== undefined ? oldValue : -1
|
|
||||||
} else {
|
|
||||||
oldValue = oldValue !== undefined ? oldValue : ''
|
|
||||||
}
|
|
||||||
$input.val(oldValue)
|
|
||||||
handleSettingValueChange($input)
|
|
||||||
})
|
|
||||||
|
|
||||||
const referencedID = window.location.hash
|
|
||||||
redirectToSetting(referencedID)
|
|
||||||
})
|
|
||||||
|
|
||||||
function saveSetting(statusNode, $input, settingName, settingValue) {
|
|
||||||
const url = '/instance/saveSetting/'
|
|
||||||
const data = {
|
|
||||||
name: settingName,
|
|
||||||
value: settingValue,
|
|
||||||
}
|
|
||||||
const APIOptions = {
|
|
||||||
statusNode: statusNode,
|
|
||||||
}
|
|
||||||
AJAXApi.quickFetchAndPostForm(url, data, APIOptions).then((result) => {
|
|
||||||
settingsFlattened[settingName] = result.data
|
|
||||||
if ($input.attr('type') == 'checkbox') {
|
|
||||||
$input.prop('checked', result.data.value)
|
|
||||||
} else {
|
|
||||||
$input.val(result.data.value)
|
|
||||||
}
|
|
||||||
handleSettingValueChange($input)
|
|
||||||
}).catch((e) => {})
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSettingValueChange($input) {
|
|
||||||
const oldValue = settingsFlattened[$input.data('setting-name')].value
|
|
||||||
const newValue = ($input.attr('type') == 'checkbox' ? $input.is(':checked') : $input.val())
|
|
||||||
if (newValue == oldValue || (newValue == '' && oldValue == undefined)) {
|
|
||||||
restoreWarnings($input)
|
|
||||||
} else {
|
|
||||||
removeWarnings($input)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeWarnings($input) {
|
|
||||||
const $inputGroup = $input.closest('.input-group')
|
|
||||||
const $btnSettingAction = $inputGroup.find('.btn-setting-action')
|
|
||||||
const $saveButton = $('.setting-group button.btn-save-setting')
|
|
||||||
$input.removeClass(['is-invalid', 'border-warning', 'border-danger', 'border-info', 'warning', 'info'])
|
|
||||||
$btnSettingAction.removeClass('d-none')
|
|
||||||
if ($input.is('select') && $input.find('option:selected').data('is-empty-option') == 1) {
|
|
||||||
$btnSettingAction.addClass('d-none') // hide save button if empty selection picked
|
|
||||||
}
|
|
||||||
$inputGroup.parent().find('.invalid-feedback').removeClass('d-block')
|
|
||||||
}
|
|
||||||
|
|
||||||
function restoreWarnings($input) {
|
|
||||||
const $inputGroup = $input.closest('.input-group')
|
|
||||||
const $btnSettingAction = $inputGroup.find('.btn-setting-action')
|
|
||||||
const $saveButton = $('.setting-group button.btn-save-setting')
|
|
||||||
const setting = settingsFlattened[$input.data('setting-name')]
|
|
||||||
if (setting.error) {
|
|
||||||
borderVariant = setting.severity !== undefined ? variantFromSeverity[setting.severity] : 'warning'
|
|
||||||
$input.addClass(['is-invalid', `border-${borderVariant}`, borderVariant])
|
|
||||||
$inputGroup.parent().find('.invalid-feedback').addClass('d-block').text(setting.errorMessage)
|
|
||||||
} else {
|
|
||||||
removeWarnings($input)
|
|
||||||
}
|
|
||||||
const $callout = $input.closest('.settings-group')
|
|
||||||
updateCalloutColors($callout)
|
|
||||||
$btnSettingAction.addClass('d-none')
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateCalloutColors($callout) {
|
|
||||||
if ($callout.length == 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const $settings = $callout.find('input, select')
|
|
||||||
const settingNames = Array.from($settings).map((i) => {
|
|
||||||
return $(i).data('setting-name')
|
|
||||||
})
|
|
||||||
const severityMapping = {null: 0, info: 1, warning: 2, critical: 3}
|
|
||||||
const severityMappingInverted = Object.assign({}, ...Object.entries(severityMapping).map(([k, v]) => ({[v]: k})))
|
|
||||||
let highestSeverity = severityMapping[null]
|
|
||||||
settingNames.forEach(name => {
|
|
||||||
if (settingsFlattened[name].error) {
|
|
||||||
highestSeverity = severityMapping[settingsFlattened[name].severity] > highestSeverity ? severityMapping[settingsFlattened[name].severity] : highestSeverity
|
|
||||||
}
|
|
||||||
});
|
|
||||||
highestSeverity = severityMappingInverted[highestSeverity]
|
|
||||||
$callout.removeClass(['callout', 'callout-danger', 'callout-warning', 'callout-info'])
|
|
||||||
if (highestSeverity !== null) {
|
|
||||||
$callout.addClass(['callout', `callout-${variantFromSeverity[highestSeverity]}`])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function redirectToSetting(referencedID) {
|
|
||||||
const $settingToFocus = $(referencedID)
|
|
||||||
const pageNavID = $(referencedID).closest('.tab-pane').attr('aria-labelledby')
|
|
||||||
const $navController = $(`#${pageNavID}`)
|
|
||||||
const $settingGroup = $settingToFocus.closest('.settings-group')
|
|
||||||
$navController
|
|
||||||
.on('shown.bs.tab.after-redirect', () => {
|
|
||||||
$settingToFocus[0].scrollIntoView()
|
|
||||||
const inputID = $settingToFocus.parent().attr('for')
|
|
||||||
$settingToFocus.closest('.setting-group').find(`#${inputID}`).focus()
|
|
||||||
$navController.off('shown.bs.tab.after-redirect')
|
|
||||||
$settingGroup.addClass(['to-be-slided', 'slide-in'])
|
|
||||||
})
|
|
||||||
.tab('show')
|
|
||||||
$settingGroup.on('webkitAnimationEnd oanimationend msAnimationEnd animationend', function() {
|
|
||||||
$(this).removeClass(['to-be-slided', 'slide-in'])
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.input-group-actions {
|
.input-group-actions {
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control[type="number"]~div>a.btn-reset-setting {
|
.form-control[type="number"]~div>a.btn-reset-setting {
|
||||||
left: -3em;
|
left: -3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
select.custom-select[multiple][data-setting-name]~span.select2-container {
|
select.custom-select[multiple][data-setting-name]~span.select2-container {
|
||||||
min-width: unset;
|
min-width: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
span.select2-container--open {
|
span.select2-container--open {
|
||||||
min-width: unset;
|
min-width: unset;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,54 +1,19 @@
|
||||||
<?php
|
<?php
|
||||||
use Cake\ORM\TableRegistry;
|
$navLinks = [];
|
||||||
|
$tabContents = [];
|
||||||
|
|
||||||
function isLeaf($setting)
|
foreach ($settingsProvider as $settingTitle => $settingContent) {
|
||||||
{
|
$navLinks[] = h($settingTitle);
|
||||||
return !empty($setting['name']) && !empty($setting['type']);
|
$tabContents[] = $this->element('Settings/category', [
|
||||||
}
|
'settings' => $settingContent,
|
||||||
|
'includeScrollspy' => false,
|
||||||
function getResolvableID($sectionName, $panelName = false)
|
|
||||||
{
|
|
||||||
$id = sprintf('sp-%s', preg_replace('/(\.|\W)/', '_', h($sectionName)));
|
|
||||||
if (!empty($panelName)) {
|
|
||||||
$id .= '-' . preg_replace('/(\.|\W)/', '_', h($panelName));
|
|
||||||
}
|
|
||||||
return $id;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$settings = [
|
|
||||||
__('Appearance') => [
|
|
||||||
'ui.bsTheme' => [
|
|
||||||
'description' => 'The Bootstrap theme to use for the application',
|
|
||||||
'default' => 'default',
|
|
||||||
'name' => 'UI Theme',
|
|
||||||
'options' => (function () {
|
|
||||||
$instanceTable = TableRegistry::getTableLocator()->get('Instance');
|
|
||||||
$themes = $instanceTable->getAvailableThemes();
|
|
||||||
return array_combine($themes, $themes);
|
|
||||||
})(),
|
|
||||||
'severity' => 'info',
|
|
||||||
'type' => 'select'
|
|
||||||
],
|
|
||||||
],
|
|
||||||
__('Bookmarks') => 'Bookmarks',
|
|
||||||
__('Account Security') => 'Account Security',
|
|
||||||
];
|
|
||||||
|
|
||||||
$cardNavs = array_keys($settings);
|
|
||||||
$cardContent = [];
|
|
||||||
|
|
||||||
$sectionHtml = '';
|
|
||||||
foreach ($settings[__('Appearance')] as $sectionName => $sectionContent) {
|
|
||||||
$sectionHtml .= $this->element('Settings/panel', [
|
|
||||||
'sectionName' => $sectionName,
|
|
||||||
'panelName' => $sectionName,
|
|
||||||
'panelSettings' => $sectionContent,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
$cardContent[] = $sectionHtml;
|
|
||||||
$cardContent[] = $settings[__('Bookmarks')];
|
$navLinks[] = __('Bookmarks');
|
||||||
$cardContent[] = $settings[__('Account Security')];
|
$tabContents[] = $this->element('UserSettings/saved-bookmarks', [
|
||||||
|
'bookmarks' => !empty($user->user_settings_by_name['ui.bookmarks']['value']) ? json_decode($user->user_settings_by_name['ui.bookmarks']['value'], true) : []
|
||||||
|
]);
|
||||||
|
|
||||||
$tabsOptions = [
|
$tabsOptions = [
|
||||||
'vertical' => true,
|
'vertical' => true,
|
||||||
|
@ -58,13 +23,19 @@ $tabsOptions = [
|
||||||
'justify' => 'center',
|
'justify' => 'center',
|
||||||
'nav-class' => ['settings-tabs'],
|
'nav-class' => ['settings-tabs'],
|
||||||
'data' => [
|
'data' => [
|
||||||
'navs' => $cardNavs,
|
'navs' => $navLinks,
|
||||||
'content' => $cardContent
|
'content' => $tabContents
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
$tabs = $this->Bootstrap->tabs($tabsOptions);
|
$tabs = $this->Bootstrap->tabs($tabsOptions);
|
||||||
|
echo $this->Html->script('settings');
|
||||||
?>
|
?>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.settingsFlattened = <?= json_encode($settingsFlattened) ?>;
|
||||||
|
window.saveSettingURL = '/userSettings/saveSetting'
|
||||||
|
</script>
|
||||||
|
|
||||||
<h2 class="fw-light"><?= __('Account settings') ?></h2>
|
<h2 class="fw-light"><?= __('Account settings') ?></h2>
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -0,0 +1,164 @@
|
||||||
|
const variantFromSeverity = {
|
||||||
|
'critical': 'danger',
|
||||||
|
'warning': 'warning',
|
||||||
|
'info': 'info',
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
if (
|
||||||
|
variantFromSeverity === undefined ||
|
||||||
|
window.settingsFlattened === undefined ||
|
||||||
|
window.saveSettingURL === undefined
|
||||||
|
) {
|
||||||
|
console.error('`settingFlatenned` and `saveSettingURL` variables must be set')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.getElementsByClassName('.depends-on-icon').length > 0) {
|
||||||
|
new bootstrap.Tooltip('.depends-on-icon', {
|
||||||
|
placement: 'right',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
$('select.custom-select[multiple]').select2()
|
||||||
|
|
||||||
|
$('.settings-tabs a[data-bs-toggle="tab"]').on('shown.bs.tab', function (event) {
|
||||||
|
$('[data-bs-spy="scroll"]').trigger('scroll.bs.scrollspy')
|
||||||
|
})
|
||||||
|
|
||||||
|
$('.tab-content input, .tab-content select').on('input', function () {
|
||||||
|
if ($(this).attr('type') == 'checkbox') {
|
||||||
|
const $input = $(this)
|
||||||
|
const $inputGroup = $(this).closest('.setting-group')
|
||||||
|
const settingName = $(this).data('setting-name')
|
||||||
|
const settingValue = $(this).is(':checked') ? 1 : 0
|
||||||
|
saveSetting($inputGroup[0], $input, settingName, settingValue)
|
||||||
|
} else {
|
||||||
|
handleSettingValueChange($(this))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
$('.tab-content .setting-group .btn-save-setting').click(function () {
|
||||||
|
const $input = $(this).closest('.input-group').find('input, select')
|
||||||
|
const settingName = $input.data('setting-name')
|
||||||
|
const settingValue = $input.val()
|
||||||
|
saveSetting(this, $input, settingName, settingValue)
|
||||||
|
})
|
||||||
|
$('.tab-content .setting-group .btn-reset-setting').click(function () {
|
||||||
|
const $btn = $(this)
|
||||||
|
const $input = $btn.closest('.input-group').find('input, select')
|
||||||
|
let oldValue = window.settingsFlattened[$input.data('setting-name')].value
|
||||||
|
if ($input.is('select')) {
|
||||||
|
oldValue = oldValue !== undefined ? oldValue : -1
|
||||||
|
} else {
|
||||||
|
oldValue = oldValue !== undefined ? oldValue : ''
|
||||||
|
}
|
||||||
|
$input.val(oldValue)
|
||||||
|
handleSettingValueChange($input)
|
||||||
|
})
|
||||||
|
|
||||||
|
const referencedID = window.location.hash
|
||||||
|
redirectToSetting(referencedID)
|
||||||
|
})
|
||||||
|
|
||||||
|
function saveSetting(statusNode, $input, settingName, settingValue) {
|
||||||
|
const url = window.saveSettingURL
|
||||||
|
const data = {
|
||||||
|
name: settingName,
|
||||||
|
value: settingValue,
|
||||||
|
}
|
||||||
|
const APIOptions = {
|
||||||
|
statusNode: statusNode,
|
||||||
|
}
|
||||||
|
AJAXApi.quickFetchAndPostForm(url, data, APIOptions).then((result) => {
|
||||||
|
window.settingsFlattened[settingName] = result.data
|
||||||
|
if ($input.attr('type') == 'checkbox') {
|
||||||
|
$input.prop('checked', result.data.value == true)
|
||||||
|
} else {
|
||||||
|
$input.val(result.data.value)
|
||||||
|
}
|
||||||
|
handleSettingValueChange($input)
|
||||||
|
}).catch((e) => { })
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSettingValueChange($input) {
|
||||||
|
let oldValue = window.settingsFlattened[$input.data('setting-name')].value
|
||||||
|
const newValue = ($input.attr('type') == 'checkbox' ? $input.is(':checked') : $input.val())
|
||||||
|
if ($input.attr('type') == 'checkbox') {
|
||||||
|
oldValue = oldValue == true
|
||||||
|
}
|
||||||
|
if (newValue == oldValue || (newValue == '' && oldValue == undefined)) {
|
||||||
|
restoreWarnings($input)
|
||||||
|
} else {
|
||||||
|
removeWarnings($input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeWarnings($input) {
|
||||||
|
const $inputGroup = $input.closest('.input-group')
|
||||||
|
const $btnSettingAction = $inputGroup.find('.btn-setting-action')
|
||||||
|
const $saveButton = $('.setting-group button.btn-save-setting')
|
||||||
|
$input.removeClass(['is-invalid', 'border-warning', 'border-danger', 'border-info', 'warning', 'info'])
|
||||||
|
$btnSettingAction.removeClass('d-none')
|
||||||
|
if ($input.is('select') && $input.find('option:selected').data('is-empty-option') == 1) {
|
||||||
|
$btnSettingAction.addClass('d-none') // hide save button if empty selection picked
|
||||||
|
}
|
||||||
|
$inputGroup.parent().find('.invalid-feedback').removeClass('d-block')
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreWarnings($input) {
|
||||||
|
const $inputGroup = $input.closest('.input-group')
|
||||||
|
const $btnSettingAction = $inputGroup.find('.btn-setting-action')
|
||||||
|
const $saveButton = $('.setting-group button.btn-save-setting')
|
||||||
|
const setting = window.settingsFlattened[$input.data('setting-name')]
|
||||||
|
if (setting.error) {
|
||||||
|
borderVariant = setting.severity !== undefined ? variantFromSeverity[setting.severity] : 'warning'
|
||||||
|
$input.addClass(['is-invalid', `border-${borderVariant}`, borderVariant])
|
||||||
|
$inputGroup.parent().find('.invalid-feedback').addClass('d-block').text(setting.errorMessage)
|
||||||
|
} else {
|
||||||
|
removeWarnings($input)
|
||||||
|
}
|
||||||
|
const $callout = $input.closest('.settings-group')
|
||||||
|
updateCalloutColors($callout)
|
||||||
|
$btnSettingAction.addClass('d-none')
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCalloutColors($callout) {
|
||||||
|
if ($callout.length == 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const $settings = $callout.find('input, select')
|
||||||
|
const settingNames = Array.from($settings).map((i) => {
|
||||||
|
return $(i).data('setting-name')
|
||||||
|
})
|
||||||
|
const severityMapping = { null: 0, info: 1, warning: 2, critical: 3 }
|
||||||
|
const severityMappingInverted = Object.assign({}, ...Object.entries(severityMapping).map(([k, v]) => ({ [v]: k })))
|
||||||
|
let highestSeverity = severityMapping[null]
|
||||||
|
settingNames.forEach(name => {
|
||||||
|
if (window.settingsFlattened[name].error) {
|
||||||
|
highestSeverity = severityMapping[window.settingsFlattened[name].severity] > highestSeverity ? severityMapping[window.settingsFlattened[name].severity] : highestSeverity
|
||||||
|
}
|
||||||
|
});
|
||||||
|
highestSeverity = severityMappingInverted[highestSeverity]
|
||||||
|
$callout.removeClass(['callout', 'callout-danger', 'callout-warning', 'callout-info'])
|
||||||
|
if (highestSeverity !== null) {
|
||||||
|
$callout.addClass(['callout', `callout-${variantFromSeverity[highestSeverity]}`])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function redirectToSetting(referencedID) {
|
||||||
|
const $settingToFocus = $(referencedID)
|
||||||
|
const pageNavID = $(referencedID).closest('.tab-pane').attr('aria-labelledby')
|
||||||
|
const $navController = $(`#${pageNavID}`)
|
||||||
|
const $settingGroup = $settingToFocus.closest('.settings-group')
|
||||||
|
$navController
|
||||||
|
.on('shown.bs.tab.after-redirect', () => {
|
||||||
|
$settingToFocus[0].scrollIntoView()
|
||||||
|
const inputID = $settingToFocus.parent().attr('for')
|
||||||
|
$settingToFocus.closest('.setting-group').find(`#${inputID}`).focus()
|
||||||
|
$navController.off('shown.bs.tab.after-redirect')
|
||||||
|
$settingGroup.addClass(['to-be-slided', 'slide-in'])
|
||||||
|
})
|
||||||
|
.tab('show')
|
||||||
|
$settingGroup.on('webkitAnimationEnd oanimationend msAnimationEnd animationend', function () {
|
||||||
|
$(this).removeClass(['to-be-slided', 'slide-in'])
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in New Issue