chg: [instance:settings] UI Improvements and framework to save settings - WiP
parent
9f4fbf0410
commit
6f4f628c7c
|
@ -111,4 +111,31 @@ class InstanceController extends AppController
|
|||
$this->set('settingsFlattened', $all['settingsFlattened']);
|
||||
$this->set('notices', $all['notices']);
|
||||
}
|
||||
|
||||
public function saveSetting()
|
||||
{
|
||||
if ($this->request->is('post')) {
|
||||
$data = $this->ParamHandler->harvestParams([
|
||||
'name',
|
||||
'value'
|
||||
]);
|
||||
$this->Settings = $this->getTableLocator()->get('Settings');
|
||||
$errors = $this->Settings->saveSetting($data['name'], $data['value']);
|
||||
$message = __('Could not save setting `{0}`', $data['name']);
|
||||
if (empty($errors)) {
|
||||
$message = __('Setting `{0}` saved', $data['name']);
|
||||
$data = $this->Settings->getSetting($data['name']);
|
||||
// TO DEL
|
||||
$data['errorMessage'] = 'Test test test';
|
||||
$data['error'] = true;
|
||||
$data['error'] = false;
|
||||
// TO DEL
|
||||
}
|
||||
$this->CRUD->setResponseForController('saveSetting', empty($errors), $message, $data, $errors);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,4 +33,19 @@ class SettingsTable extends AppTable
|
|||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function getSetting($name=false): array
|
||||
{
|
||||
$settings = Configure::read()['Cerebrate'];
|
||||
$settingsProvider = $this->SettingsProvider->getSettingsConfiguration($settings);
|
||||
$settingsFlattened = $this->SettingsProvider->flattenSettingsConfiguration($settingsProvider);
|
||||
return $settingsFlattened[$name] ?? [];
|
||||
}
|
||||
|
||||
public function saveSetting(string $name, string $value): array
|
||||
{
|
||||
$errors = [];
|
||||
// Save setting here!
|
||||
return $errors;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
echo $this->element('genericElements/Form/genericForm', [
|
||||
'data' => [
|
||||
'description' => __('Authkeys are used for API access. A user can have more than one authkey, so if you would like to use separate keys per tool that queries Cerebrate, add additional keys. Use the comment field to make identifying your keys easier.'),
|
||||
'fields' => [
|
||||
[
|
||||
'field' => 'name',
|
||||
],
|
||||
[
|
||||
'field' => 'value'
|
||||
],
|
||||
],
|
||||
'submit' => [
|
||||
'action' => $this->request->getParam('action')
|
||||
]
|
||||
]
|
||||
]);
|
|
@ -92,7 +92,7 @@ function genLevel0($settingsProvider, $appView)
|
|||
$content0[] = __('No Settings available yet');
|
||||
}
|
||||
}
|
||||
array_unshift($level0, __('Setting Diagnostic'));
|
||||
array_unshift($level0, __('Settings Diagnostic'));
|
||||
array_unshift($content0, $appView->get('settingNotice'));
|
||||
$tabsOptions0 = [
|
||||
// 'vertical' => true,
|
||||
|
@ -178,6 +178,7 @@ function genLevel3($level2Name, $settingGroupName, $setting, $appView)
|
|||
'p-2',
|
||||
'mb-4',
|
||||
'rounded',
|
||||
'settings-group',
|
||||
(!empty($groupIssueSeverity) ? "callout callout-${groupIssueSeverity}" : ''),
|
||||
($appView->get('darkMode') ? 'bg-dark' : 'bg-light')
|
||||
],
|
||||
|
@ -211,56 +212,75 @@ function genSingleSetting($settingName, $setting, $appView)
|
|||
'id' => "{$settingId}Help"
|
||||
], h($setting['description']));
|
||||
}
|
||||
$error = '';
|
||||
if (!empty($setting['error'])) {
|
||||
$textColor = '';
|
||||
$textColor = 'text-warning';
|
||||
if (!empty($setting['severity'])) {
|
||||
$textColor = "text-{$appView->get('variantFromSeverity')[$setting['severity']]}";
|
||||
$error = $appView->Bootstrap->genNode('div', [
|
||||
'class' => ['d-block', 'invalid-feedback', $textColor],
|
||||
], h($setting['errorMessage']));
|
||||
}
|
||||
$error = $appView->Bootstrap->genNode('div', [
|
||||
'class' => ['d-block', 'invalid-feedback', $textColor],
|
||||
], (!empty($setting['error']) ? h($setting['errorMessage']) : ''));
|
||||
if (empty($setting['type'])) {
|
||||
$setting['type'] = 'string';
|
||||
}
|
||||
if ($setting['type'] == 'string') {
|
||||
$input = genInputString($settingId, $setting, $appView);
|
||||
$input = genInputString($settingName, $setting, $appView);
|
||||
} elseif ($setting['type'] == 'boolean') {
|
||||
$input = genInputCheckbox($settingId, $setting, $appView);
|
||||
$input = genInputCheckbox($settingName, $setting, $appView);
|
||||
$description = '';
|
||||
} elseif ($setting['type'] == 'integer') {
|
||||
$input = genInputInteger($settingId, $setting, $appView);
|
||||
$input = genInputInteger($settingName, $setting, $appView);
|
||||
} elseif ($setting['type'] == 'select') {
|
||||
$input = genInputSelect($settingId, $setting, $appView);
|
||||
$input = genInputSelect($settingName, $setting, $appView);
|
||||
} elseif ($setting['type'] == 'multi-select') {
|
||||
$input = genInputMultiSelect($settingId, $setting, $appView);
|
||||
$input = genInputMultiSelect($settingName, $setting, $appView);
|
||||
} else {
|
||||
$input = genInputString($settingId, $setting, $appView);
|
||||
$input = genInputString($settingName, $setting, $appView);
|
||||
}
|
||||
|
||||
$inputGroupSave = $appView->Bootstrap->genNode('div', [
|
||||
'class' => ['input-group-append', 'd-none', 'position-relative', 'input-group-actions'],
|
||||
], implode('', [
|
||||
$appView->Bootstrap->genNode('a', [
|
||||
'class' => ['position-absolute', 'fas fa-times', 'p-abs-center-y', 'text-reset text-decoration-none', 'btn-reset-setting'],
|
||||
'href' => '#',
|
||||
'style' => 'left: -1.25em; z-index: 5;'
|
||||
]),
|
||||
$appView->Bootstrap->genNode('button', [
|
||||
'class' => ['btn', 'btn-success', 'btn-save-setting'],
|
||||
'type' => 'button'
|
||||
], __('save')),
|
||||
]));
|
||||
$inputGroup = $appView->Bootstrap->genNode('div', [
|
||||
'class' => ['input-group'],
|
||||
], implode('', [$input, $inputGroupSave]));
|
||||
|
||||
$container = $appView->Bootstrap->genNode('div', [
|
||||
'class' => ['form-group', 'mb-2']
|
||||
], implode('', [$label, $input, $description, $error]));
|
||||
], implode('', [$label, $inputGroup, $description, $error]));
|
||||
return $container;
|
||||
}
|
||||
|
||||
function genInputString($settingId, $setting, $appView)
|
||||
function genInputString($settingName, $setting, $appView)
|
||||
{
|
||||
$settingId = str_replace('.', '_', $settingName);
|
||||
return $appView->Bootstrap->genNode('input', [
|
||||
'class' => [
|
||||
'form-control',
|
||||
"xxx-{$appView->get('variantFromSeverity')[$setting['severity']]} yyy-{$setting['severity']}",
|
||||
(!empty($setting['error']) ? 'is-invalid' : ''),
|
||||
(!empty($setting['error']) ? "border-{$appView->get('variantFromSeverity')[$setting['severity']]}" : ''),
|
||||
(!empty($setting['error']) && $setting['severity'] == 'warning' ? 'warning' : ''),
|
||||
],
|
||||
'type' => 'text',
|
||||
'id' => $settingId,
|
||||
'data-setting-name' => $settingName,
|
||||
'value' => isset($setting['value']) ? $setting['value'] : "",
|
||||
'placeholder' => $setting['default'] ?? '',
|
||||
'aria-describedby' => "{$settingId}Help"
|
||||
]);
|
||||
}
|
||||
function genInputCheckbox($settingId, $setting, $appView)
|
||||
function genInputCheckbox($settingName, $setting, $appView)
|
||||
{
|
||||
$settingId = str_replace('.', '_', $settingName);
|
||||
$switch = $appView->Bootstrap->genNode('input', [
|
||||
'class' => [
|
||||
'custom-control-input'
|
||||
|
@ -269,6 +289,7 @@ function genInputCheckbox($settingId, $setting, $appView)
|
|||
'value' => !empty($setting['value']) ? 1 : 0,
|
||||
'checked' => !empty($setting['value']) ? 'checked' : '',
|
||||
'id' => $settingId,
|
||||
'data-setting-name' => $settingName,
|
||||
]);
|
||||
$label = $appView->Bootstrap->genNode('label', [
|
||||
'class' => [
|
||||
|
@ -284,8 +305,9 @@ function genInputCheckbox($settingId, $setting, $appView)
|
|||
], implode('', [$switch, $label]));
|
||||
return $container;
|
||||
}
|
||||
function genInputInteger($settingId, $setting, $appView)
|
||||
function genInputInteger($settingName, $setting, $appView)
|
||||
{
|
||||
$settingId = str_replace('.', '_', $settingName);
|
||||
return $appView->Bootstrap->genNode('input', [
|
||||
'class' => [
|
||||
'form-control'
|
||||
|
@ -294,6 +316,7 @@ function genInputInteger($settingId, $setting, $appView)
|
|||
'min' => '0',
|
||||
'step' => 1,
|
||||
'id' => $settingId,
|
||||
'data-setting-name' => $settingName,
|
||||
'aria-describedby' => "{$settingId}Help"
|
||||
]);
|
||||
}
|
||||
|
@ -330,6 +353,7 @@ function isLeaf($setting)
|
|||
|
||||
|
||||
<script>
|
||||
const variantFromSeverity = <?= json_encode($variantFromSeverity) ?>;
|
||||
const settingsFlattened = <?= json_encode($settingsFlattened) ?>;
|
||||
let selectData = []
|
||||
for (const settingName in settingsFlattened) {
|
||||
|
@ -391,6 +415,36 @@ function isLeaf($setting)
|
|||
}
|
||||
$("#search-settings").val(null).trigger('change.select2');
|
||||
})
|
||||
|
||||
$('.tab-content input').on('input', function() {
|
||||
handleSettingValueChange($(this))
|
||||
})
|
||||
|
||||
$('.tab-content .input-group-actions .btn-save-setting').click(function() {
|
||||
const $input = $(this).closest('.input-group').find('input')
|
||||
const settingName = $input.data('setting-name')
|
||||
const settingValue = $input.val()
|
||||
const url = '/instance/saveSetting/'
|
||||
const data = {
|
||||
name: settingName,
|
||||
value: settingValue,
|
||||
}
|
||||
const APIOptions = {
|
||||
statusNode: this
|
||||
}
|
||||
AJAXApi.quickFetchAndPostForm(url, data, APIOptions).then((result) => {
|
||||
settingsFlattened[settingName] = result.data
|
||||
$input.val(result.data.value)
|
||||
handleSettingValueChange($input)
|
||||
})
|
||||
})
|
||||
$('.tab-content .input-group-actions .btn-reset-setting').click(function() {
|
||||
const $btn = $(this)
|
||||
const $input = $btn.closest('.input-group').find('input')
|
||||
const oldValue = settingsFlattened[$input.data('setting-name')].value
|
||||
$input.val(oldValue)
|
||||
handleSettingValueChange($input)
|
||||
})
|
||||
})
|
||||
|
||||
function formatSettingSearchResult(state) {
|
||||
|
@ -412,6 +466,62 @@ function isLeaf($setting)
|
|||
return state.text
|
||||
}
|
||||
|
||||
function handleSettingValueChange($input) {
|
||||
const oldValue = settingsFlattened[$input.data('setting-name')].value
|
||||
if ($input.val() == oldValue) {
|
||||
restoreWarnings($input)
|
||||
} else {
|
||||
removeWarnings($input)
|
||||
}
|
||||
}
|
||||
|
||||
function removeWarnings($input) {
|
||||
const $inputGroup = $input.closest('.input-group')
|
||||
const $inputGroupAppend = $inputGroup.find('.input-group-append')
|
||||
const $saveButton = $inputGroup.find('button.btn-save-setting')
|
||||
$input.removeClass(['is-invalid', 'border-warning', 'border-danger'])
|
||||
$inputGroupAppend.removeClass('d-none')
|
||||
$inputGroup.parent().find('.invalid-feedback').removeClass('d-block')
|
||||
}
|
||||
|
||||
function restoreWarnings($input) {
|
||||
const $inputGroup = $input.closest('.input-group')
|
||||
const $inputGroupAppend = $inputGroup.find('.input-group-append')
|
||||
const $saveButton = $inputGroup.find('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}`])
|
||||
if (setting.severity == 'warning') {
|
||||
$input.addClass('warning')
|
||||
}
|
||||
$inputGroup.parent().find('.invalid-feedback').addClass('d-block').text(setting.errorMessage)
|
||||
}
|
||||
const $callout = $input.closest('.settings-group')
|
||||
updateCalloutColors($callout)
|
||||
$inputGroupAppend.addClass('d-none')
|
||||
}
|
||||
|
||||
function updateCalloutColors($callout) {
|
||||
const $settings = $callout.find('input')
|
||||
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]}`])
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -140,4 +140,20 @@ div.progress-timeline .progress-line.progress-inactive {
|
|||
|
||||
.form-control.is-invalid.warning {
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23ffc107' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ffc107' stroke='none'/%3e%3c/svg%3e")
|
||||
}
|
||||
|
||||
.p-abs-center-y {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.p-abs-center-x {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.p-abs-center-both {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
}
|
|
@ -149,6 +149,7 @@ class AJAXApi {
|
|||
/**
|
||||
* @param {string} url - The URL from which to fetch the form
|
||||
* @param {Object} [dataToMerge={}] - Additional data to be integrated or modified in the form
|
||||
* @param {Object} [options={}] - The options supported by AJAXApi#defaultOptions
|
||||
* @return {Promise<Object>} Promise object resolving to the result of the POST operation
|
||||
*/
|
||||
static async quickFetchAndPostForm(url, dataToMerge={}, options={}) {
|
||||
|
|
Loading…
Reference in New Issue