new: [settings] Added setting and settingProvider functionality - WiP
parent
51dd0434cd
commit
dc5d54c30e
|
@ -101,4 +101,12 @@ class InstanceController extends AppController
|
|||
$this->set('path', ['controller' => 'instance', 'action' => 'rollback']);
|
||||
$this->render('/genericTemplates/confirm');
|
||||
}
|
||||
|
||||
public function settings()
|
||||
{
|
||||
$this->Settings = $this->getTableLocator()->get('Settings');
|
||||
$all = $this->Settings->getSettings();
|
||||
$this->set('settingsProvider', $all['settingsProvider']);
|
||||
$this->set('settings', $all['settings']);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,189 @@
|
|||
<?php
|
||||
namespace App\Model\Table;
|
||||
|
||||
use App\Model\Table\AppTable;
|
||||
use Cake\Validation\Validator;
|
||||
|
||||
class SettingsProviderTable extends AppTable
|
||||
{
|
||||
private $settingsConfiguration = [];
|
||||
|
||||
public function initialize(array $config): void
|
||||
{
|
||||
parent::initialize($config);
|
||||
$this->settingsConfiguration = $this->generateSettingsConfiguration();
|
||||
$this->setTable(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* getSettingsConfiguration Return the setting configuration and merge existing settings into it if provided
|
||||
*
|
||||
* @param null|array $settings - Settings to be merged in the provided setting configuration
|
||||
* @return array
|
||||
*/
|
||||
public function getSettingsConfiguration($settings = null) {
|
||||
$settingConf = $this->settingsConfiguration;
|
||||
if (!is_null($settings)) {
|
||||
$settingConf = $this->mergeSettingsIntoSettingConfiguration($settingConf, $settings);
|
||||
}
|
||||
return $settingConf;
|
||||
}
|
||||
|
||||
private function mergeSettingsIntoSettingConfiguration($settingConf, $settings)
|
||||
{
|
||||
foreach ($settingConf as $key => $value) {
|
||||
if ($this->isLeaf($value)) {
|
||||
if (isset($settings[$key])) {
|
||||
$settingConf[$key]['value'] = $settings[$key];
|
||||
}
|
||||
} else {
|
||||
$settingConf[$key] = $this->mergeSettingsIntoSettingConfiguration($value, $settings);
|
||||
}
|
||||
}
|
||||
return $settingConf;
|
||||
}
|
||||
|
||||
private function isLeaf($setting)
|
||||
{
|
||||
return !empty($setting['name']) && !empty($setting['type']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Support up to 3 level:
|
||||
* Application -> Network -> Proxy -> Proxy.URL
|
||||
*
|
||||
* Leave errorMessage empty to let the validator generate the error message
|
||||
*/
|
||||
private function generateSettingsConfiguration()
|
||||
{
|
||||
return [
|
||||
'Application' => [
|
||||
'General' => [
|
||||
'Essentials' => [
|
||||
'baseurl' => [
|
||||
'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.'),
|
||||
'errorMessage' => __('The currently set baseurl does not match the URL through which you have accessed the page. Disregard this if you are accessing the page via an alternate URL (for example via IP address).'),
|
||||
'default' => '',
|
||||
'name' => __('Base URL'),
|
||||
'test' => 'testBaseURL',
|
||||
'type' => 'string',
|
||||
],
|
||||
'uuid' => [
|
||||
'description' => __('The Cerebrate instance UUID. This UUID is used to identify this instance.'),
|
||||
'errorMessage' => __('No valid UUID set'),
|
||||
'default' => '',
|
||||
'name' => 'UUID',
|
||||
'test' => 'testUuid',
|
||||
'type' => 'string'
|
||||
],
|
||||
],
|
||||
'Miscellaneous' => [
|
||||
'to-del' => [
|
||||
'description' => 'to del',
|
||||
'errorMessage' => 'to del',
|
||||
'default' => '',
|
||||
'name' => 'To DEL',
|
||||
'type' => 'string'
|
||||
],
|
||||
'to-del2' => [
|
||||
'description' => 'to del',
|
||||
'errorMessage' => 'to del',
|
||||
'default' => '',
|
||||
'name' => 'To DEL',
|
||||
'type' => 'string'
|
||||
],
|
||||
'to-del3' => [
|
||||
'description' => 'to del',
|
||||
'errorMessage' => 'to del',
|
||||
'default' => '',
|
||||
'name' => 'To DEL',
|
||||
'type' => 'string'
|
||||
],
|
||||
],
|
||||
'floating-setting' => [
|
||||
'description' => 'floaringSetting',
|
||||
'errorMessage' => 'floaringSetting',
|
||||
'default' => '',
|
||||
'name' => 'Uncategorized Setting',
|
||||
'type' => 'string'
|
||||
],
|
||||
],
|
||||
'Network' => [
|
||||
'Proxy' => [
|
||||
'host' => [
|
||||
'description' => __('The hostname of an HTTP proxy for outgoing sync requests. Leave empty to not use a proxy.'),
|
||||
'default' => '',
|
||||
'name' => __('Host'),
|
||||
'test' => 'testHostname',
|
||||
'type' => 'string',
|
||||
],
|
||||
'port' => [
|
||||
'description' => __('The TCP port for the HTTP proxy.'),
|
||||
'default' => '',
|
||||
'name' => __('Port'),
|
||||
'test' => 'testForRangeXY',
|
||||
'type' => 'integer',
|
||||
],
|
||||
'user' => [
|
||||
'description' => __('The authentication username for the HTTP proxy.'),
|
||||
'default' => '',
|
||||
'name' => __('User'),
|
||||
'test' => 'testForEmpty',
|
||||
'type' => 'string',
|
||||
],
|
||||
'password' => [
|
||||
'description' => __('The authentication password for the HTTP proxy.'),
|
||||
'default' => '',
|
||||
'name' => __('Password'),
|
||||
'test' => 'testForEmpty',
|
||||
'type' => 'string',
|
||||
],
|
||||
],
|
||||
'Proxy2' => [
|
||||
'host' => [
|
||||
'description' => __('The hostname of an HTTP proxy for outgoing sync requests. Leave empty to not use a proxy.'),
|
||||
'default' => '',
|
||||
'name' => __('Host'),
|
||||
'test' => 'testHostname',
|
||||
'type' => 'string',
|
||||
],
|
||||
'port' => [
|
||||
'description' => __('The TCP port for the HTTP proxy.'),
|
||||
'default' => '',
|
||||
'name' => __('Port'),
|
||||
'test' => 'testForRangeXY',
|
||||
'type' => 'integer',
|
||||
],
|
||||
'user' => [
|
||||
'description' => __('The authentication username for the HTTP proxy.'),
|
||||
'default' => '',
|
||||
'name' => __('User'),
|
||||
'test' => 'testForEmpty',
|
||||
'type' => 'string',
|
||||
],
|
||||
'password' => [
|
||||
'description' => __('The authentication password for the HTTP proxy.'),
|
||||
'default' => '',
|
||||
'name' => __('Password'),
|
||||
'test' => 'testForEmpty',
|
||||
'type' => 'string',
|
||||
],
|
||||
],
|
||||
],
|
||||
'UI' => [
|
||||
'dark' => [
|
||||
'description' => __('Enable the dark theme of the application'),
|
||||
'default' => false,
|
||||
'name' => __('Dark theme'),
|
||||
'type' => 'boolean',
|
||||
],
|
||||
],
|
||||
],
|
||||
'Features' => [
|
||||
],
|
||||
'Security' => [
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
namespace App\Model\Table;
|
||||
|
||||
use App\Model\Table\AppTable;
|
||||
use Cake\ORM\Table;
|
||||
use Cake\Validation\Validator;
|
||||
use Cake\Core\Configure;
|
||||
use Cake\ORM\TableRegistry;
|
||||
|
||||
class SettingsTable extends AppTable
|
||||
{
|
||||
public function initialize(array $config): void
|
||||
{
|
||||
parent::initialize($config);
|
||||
$this->setTable(false);
|
||||
$this->SettingsProvider = TableRegistry::getTableLocator()->get('SettingsProvider');
|
||||
}
|
||||
|
||||
public function getSettings(): array
|
||||
{
|
||||
$settings = Configure::read()['Cerebrate'];
|
||||
$settingsProvider = $this->SettingsProvider->getSettingsConfiguration($settings);
|
||||
return [
|
||||
'settings' => $settings,
|
||||
'settingsProvider' => $settingsProvider
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,268 @@
|
|||
<?php
|
||||
// debug($settings);
|
||||
// debug($settingsProvider);
|
||||
$settingTable = genLevel0($settingsProvider, $this);
|
||||
?>
|
||||
<div class="px-5">
|
||||
<div class="mb-3">
|
||||
<input class="form-control" type="text" id="search" placeholder="<?= __('Search settings') ?>" aria-describedby="<?= __('Search setting input') ?>">
|
||||
</div>
|
||||
<?= $settingTable; ?>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
function genLevel0($settingsProvider, $appView)
|
||||
{
|
||||
$content0 = [];
|
||||
$level0 = array_keys($settingsProvider);
|
||||
foreach ($settingsProvider as $level1Name => $level1Setting) {
|
||||
if (!empty($level1Setting)) {
|
||||
$content0[] = genLevel1($level1Setting, $appView);
|
||||
} else {
|
||||
$content0[] = __('No Settings available yet');
|
||||
}
|
||||
}
|
||||
$tabsOptions0 = [
|
||||
// 'vertical' => true,
|
||||
// 'vertical-size' => 2,
|
||||
'card' => false,
|
||||
'pills' => false,
|
||||
'justify' => 'center',
|
||||
'content-class' => ['mt-2'],
|
||||
'data' => [
|
||||
'navs' => $level0,
|
||||
'content' => $content0
|
||||
]
|
||||
];
|
||||
$table0 = $appView->Bootstrap->tabs($tabsOptions0);
|
||||
return $table0;
|
||||
}
|
||||
|
||||
function genLevel1($level1Setting, $appView)
|
||||
{
|
||||
$content1 = [];
|
||||
$nav1 = [];
|
||||
foreach ($level1Setting as $level2Name => $level2Setting) {
|
||||
if (!empty($level2Setting)) {
|
||||
$content1[] = genLevel2($level2Name, $level2Setting, $appView);
|
||||
} else {
|
||||
$content1[] = '';
|
||||
}
|
||||
$nav1[$level2Name] = array_filter( // only show grouped settings
|
||||
array_keys($level2Setting),
|
||||
function ($settingGroupName) use ($level2Setting) {
|
||||
return !isLeaf($level2Setting[$settingGroupName]);
|
||||
}
|
||||
);
|
||||
}
|
||||
$contentHtml = implode('', $content1);
|
||||
$scrollspyNav = genScrollspyNav($nav1);
|
||||
$mainPanelHeight = 'calc(100vh - 8px - 42px - 1rem - 56px - 38px - 1rem)';
|
||||
$container = '<div class="d-flex">';
|
||||
$container .= "<div class=\"\" style=\"flex: 0 0 10em;\">{$scrollspyNav}</div>";
|
||||
$container .= "<div data-spy=\"scroll\" data-target=\"#navbar-scrollspy-setting\" data-offset=\"24\" style=\"height: {$mainPanelHeight}\" class=\"p-3 overflow-auto position-relative flex-grow-1\">{$contentHtml}</div>";
|
||||
$container .= '</div>';
|
||||
return $container;
|
||||
}
|
||||
|
||||
function genLevel2($level2Name, $level2Setting, $appView)
|
||||
{
|
||||
foreach ($level2Setting as $level3Name => $level3Setting) {
|
||||
if (!empty($level3Setting)) {
|
||||
$level3 = genLevel3($level2Name, $level3Name, $level3Setting, $appView);
|
||||
$content2[] = sprintf('<div id="%s">%s</div>', sprintf('sp-%s', h($level2Name)), $level3);
|
||||
} else {
|
||||
$content2[] = '';
|
||||
}
|
||||
}
|
||||
return implode('', $content2);
|
||||
}
|
||||
|
||||
function genLevel3($level2Name, $settingGroupName, $setting, $appView)
|
||||
{
|
||||
$settingGroup = '';
|
||||
if (isLeaf($setting)) {
|
||||
$tmp = genSingleSetting($settingGroupName, $setting, $appView);
|
||||
$settingGroup = "<div>{$tmp}</div>";
|
||||
} else {
|
||||
$tmpID = sprintf('sp-%s-%s', h($level2Name), h($settingGroupName));
|
||||
$settingGroup .= sprintf('<h4 id="%s"><a class="text-reset text-decoration-none" href="#%s">%s</a></h4>', $tmpID, $tmpID, h($settingGroupName));
|
||||
foreach ($setting as $singleSettingName => $singleSetting) {
|
||||
$tmp = genSingleSetting($singleSettingName, $singleSetting, $appView);
|
||||
$settingGroup .= sprintf('<div class="ml-3">%s</div>', $tmp);
|
||||
}
|
||||
$settingGroup = $appView->Bootstrap->genNode('div', [
|
||||
'class' => ['shadow', 'p-2', 'mb-4', 'rounded', ($appView->get('darkMode') ? 'bg-dark' : 'bg-light')],
|
||||
], $settingGroup);
|
||||
}
|
||||
return $settingGroup;
|
||||
}
|
||||
|
||||
function genSingleSetting($settingName, $setting, $appView)
|
||||
{
|
||||
$label = $appView->Bootstrap->genNode('label', [
|
||||
'class' => ['font-weight-bolder', 'mb-0'],
|
||||
'for' => $settingName
|
||||
], h($setting['name']));
|
||||
$description = '';
|
||||
if (!empty($setting['description'])) {
|
||||
$description = $appView->Bootstrap->genNode('small', [
|
||||
'class' => ['form-text', 'text-muted', 'mt-0'],
|
||||
'id' => "{$settingName}Help"
|
||||
], h($setting['description']));
|
||||
}
|
||||
$inputGroup = '';
|
||||
if (empty($setting['type'])) {
|
||||
$setting['type'] = 'string';
|
||||
}
|
||||
if ($setting['type'] == 'string') {
|
||||
$input = genInputString($settingName, $setting, $appView);
|
||||
} elseif ($setting['type'] == 'boolean') {
|
||||
$input = genInputCheckbox($settingName, $setting, $appView);
|
||||
$description = '';
|
||||
} elseif ($setting['type'] == 'integer') {
|
||||
$input = genInputInteger($settingName, $setting, $appView);
|
||||
} elseif ($setting['type'] == 'select') {
|
||||
$input = genInputSelect($settingName, $setting, $appView);
|
||||
} elseif ($setting['type'] == 'multi-select') {
|
||||
$input = genInputMultiSelect($settingName, $setting, $appView);
|
||||
} else {
|
||||
$input = genInputString($settingName, $setting, $appView);
|
||||
}
|
||||
$container = $appView->Bootstrap->genNode('div', [
|
||||
'class' => ['form-group', 'mb-2']
|
||||
], implode('', [$label, $input, $description]));
|
||||
return $container;
|
||||
}
|
||||
|
||||
function genInputString($settingName, $setting, $appView)
|
||||
{
|
||||
return $appView->Bootstrap->genNode('input', [
|
||||
'class' => [
|
||||
'form-control'
|
||||
],
|
||||
'type' => 'text',
|
||||
'id' => $settingName,
|
||||
'value' => isset($setting['value']) ? $setting['value'] : "",
|
||||
'aria-describedby' => "{$settingName}Help"
|
||||
]);
|
||||
}
|
||||
function genInputCheckbox($settingName, $setting, $appView)
|
||||
{
|
||||
$switch = $appView->Bootstrap->genNode('input', [
|
||||
'class' => [
|
||||
'custom-control-input'
|
||||
],
|
||||
'type' => 'checkbox',
|
||||
'value' => !empty($setting['value']) ? 1 : 0,
|
||||
'checked' => !empty($setting['value']) ? 'checked' : '',
|
||||
'id' => $settingName,
|
||||
]);
|
||||
$label = $appView->Bootstrap->genNode('label', [
|
||||
'class' => [
|
||||
'custom-control-label'
|
||||
],
|
||||
'for' => $settingName,
|
||||
], h($setting['description']));
|
||||
$container = $appView->Bootstrap->genNode('div', [
|
||||
'class' => [
|
||||
'custom-control',
|
||||
'custom-switch',
|
||||
],
|
||||
], implode('', [$switch, $label]));
|
||||
return $container;
|
||||
}
|
||||
function genInputInteger($settingName, $setting, $appView)
|
||||
{
|
||||
return $appView->Bootstrap->genNode('input', [
|
||||
'class' => [
|
||||
'form-control'
|
||||
],
|
||||
'params' => [
|
||||
'type' => 'integer',
|
||||
'id' => $settingName,
|
||||
'aria-describedby' => "{$settingName}Help"
|
||||
]
|
||||
]);
|
||||
}
|
||||
function genInputSelect($settingName, $setting, $appView)
|
||||
{
|
||||
}
|
||||
function genInputMultiSelect($settingName, $setting, $appView)
|
||||
{
|
||||
}
|
||||
|
||||
function genScrollspyNav($nav1)
|
||||
{
|
||||
$nav = '<nav id="navbar-scrollspy-setting" class="navbar">';
|
||||
$nav .= '<nav class="nav nav-pills flex-column">';
|
||||
foreach ($nav1 as $group => $sections) {
|
||||
$nav .= sprintf('<a class="nav-link main-group text-reset p-1" href="#%s">%s</a>', sprintf('sp-%s', h($group)), h($group));
|
||||
$nav .= sprintf('<nav class="nav nav-pills sub-group collapse flex-column" data-maingroup="%s">', sprintf('sp-%s', h($group)));
|
||||
foreach ($sections as $section) {
|
||||
$nav .= sprintf('<a class="nav-link nav-link-group text-reset ml-3 my-1 p-1" href="#%s">%s</a>', sprintf('sp-%s-%s', h($group), h($section)), h($section));
|
||||
}
|
||||
$nav .= '</nav>';
|
||||
}
|
||||
$nav .= '</nav>';
|
||||
$nav .= '</nav>';
|
||||
return $nav;
|
||||
}
|
||||
|
||||
function isLeaf($setting)
|
||||
{
|
||||
return !empty($setting['name']) && !empty($setting['type']);
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('[data-spy="scroll"]').on('activate.bs.scrollspy', function(evt, {relatedTarget}) {
|
||||
const $associatedLink = $(`#navbar-scrollspy-setting nav.nav-pills .nav-link[href="${relatedTarget}"]`)
|
||||
let $associatedNav
|
||||
if ($associatedLink.hasClass('main-group')) {
|
||||
$associatedNav = $associatedLink.next()
|
||||
} else {
|
||||
$associatedNav = $associatedLink.parent()
|
||||
}
|
||||
const $allNavs = $('#navbar-scrollspy-setting nav.nav-pills.sub-group')
|
||||
$allNavs.removeClass('group-active').hide()
|
||||
$associatedNav.addClass('group-active').show()
|
||||
})
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#navbar-scrollspy-setting nav.nav-pills .nav-link {
|
||||
background-color: unset !important;
|
||||
color: black;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#navbar-scrollspy-setting nav.nav-pills .nav-link:not(.main-group).active {
|
||||
color: #007bff !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#navbar-scrollspy-setting nav.nav-pills .nav-link.main-group:before {
|
||||
margin-right: 0.25em;
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-weight: 900;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
display: inline-block;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
text-rendering: auto;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
#navbar-scrollspy-setting nav.nav-pills .nav-link.main-group.active:before {
|
||||
content: "\f0d7";
|
||||
}
|
||||
|
||||
#navbar-scrollspy-setting nav.nav-pills .nav-link.main-group:before {
|
||||
content: "\f0da";
|
||||
}
|
||||
</style>
|
Loading…
Reference in New Issue