mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-13 03:45:11 +08:00
1384 lines
47 KiB
JavaScript
1384 lines
47 KiB
JavaScript
/* SPDX-License-Identifier: Apache-2.0
|
||
*
|
||
* luci-app-sysctl - LuCI view for kernel sysctl parameters (OpenWrt 24.10)
|
||
* v1.4.0: modal-free architecture - all views, editors, confirmations and
|
||
* results render inline on the page (robust against theme quirks on
|
||
* third-party firmwares where ui.showModal may be invisible).
|
||
*
|
||
* Section 1: custom parameters (/etc/sysctl.d/99-luci-sysctl.conf)
|
||
* Section 2: online preset (/etc/sysctl.d/98-online-preset.conf)
|
||
* Section 3: browse/search live kernel parameters from /proc/sys
|
||
*/
|
||
|
||
'use strict';
|
||
'require view';
|
||
'require rpc';
|
||
'require ui';
|
||
'require dom';
|
||
|
||
var callList = rpc.declare({ object: 'luci.sysctl', method: 'list' });
|
||
var callStatus = rpc.declare({ object: 'luci.sysctl', method: 'status' });
|
||
var callBrowse = rpc.declare({ object: 'luci.sysctl', method: 'browse', params: [ 'prefix' ] });
|
||
var callSearch = rpc.declare({ object: 'luci.sysctl', method: 'search', params: [ 'query', 'limit' ] });
|
||
var callSet = rpc.declare({ object: 'luci.sysctl', method: 'set', params: [ 'key', 'value', 'disabled', 'apply' ] });
|
||
var callRemove = rpc.declare({ object: 'luci.sysctl', method: 'remove', params: [ 'key' ] });
|
||
var callApply = rpc.declare({ object: 'luci.sysctl', method: 'apply' });
|
||
var callPresetStatus = rpc.declare({ object: 'luci.sysctl', method: 'preset_status' });
|
||
var callPresetFetch = rpc.declare({ object: 'luci.sysctl', method: 'preset_fetch', params: [ 'url', 'prefix' ] });
|
||
var callPresetImport = rpc.declare({ object: 'luci.sysctl', method: 'preset_import', params: [ 'url', 'prefix' ] });
|
||
var callPresetCheck = rpc.declare({ object: 'luci.sysctl', method: 'preset_check' });
|
||
var callPresetRemove = rpc.declare({
|
||
object: 'luci.sysctl',
|
||
method: 'preset_remove'
|
||
});
|
||
|
||
var callPresetList = rpc.declare({
|
||
object: 'luci.sysctl',
|
||
method: 'preset_list',
|
||
params: [ 'url', 'prefix' ]
|
||
});
|
||
var callFileView = rpc.declare({ object: 'luci.sysctl', method: 'file_view', params: [ 'path' ] });
|
||
var callFileSet = rpc.declare({ object: 'luci.sysctl', method: 'file_set', params: [ 'path', 'key', 'value', 'disabled' ] });
|
||
var callFileDelete = rpc.declare({ object: 'luci.sysctl', method: 'file_delete', params: [ 'path', 'key' ] });
|
||
|
||
var KEY_RE = /^[A-Za-z0-9_][A-Za-z0-9_.\/-]*$/;
|
||
|
||
function badge(text, kind) {
|
||
return E('span', { 'class': 'lsc-badge lsc-badge-' + kind }, text);
|
||
}
|
||
|
||
var LSC_CSS = [
|
||
'.lsc-root { font-size: 15px; }',
|
||
'.lsc-root .lsc-chips { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin: 6px 0 2px; }',
|
||
'.lsc-root .lsc-chip { appearance: none; border: 1px solid #d0d7de; background: #fff; color: #24292f; border-radius: 999px; padding: 4px 12px; font-size: 12.5px; line-height: 1.5; cursor: pointer; text-transform: none; font-weight: 500; transition: background .15s, color .15s, border-color .15s, box-shadow .15s; box-shadow: 0 1px 2px rgba(16,24,40,.04); }',
|
||
'.lsc-root .lsc-chip:hover { border-color: #0969da; color: #0969da; background: #f3f8ff; }',
|
||
'.lsc-root .lsc-chip.lsc-chip-active { background: #0969da; border-color: #0969da; color: #fff; }',
|
||
'.lsc-root .lsc-tablewrap { border: 1px solid #e4e7ec; border-radius: 8px; overflow: auto; background: #fff; margin: 8px 0; }',
|
||
'.lsc-root table.lsc-table { width: 100%; border-collapse: collapse; font-size: 15px; margin: 0; }',
|
||
'.lsc-root .lsc-table thead th { background: #f6f8fa; text-align: center !important; font-weight: 600; color: #57606a; padding: 10px 12px; border-bottom: 1px solid #e4e7ec; white-space: nowrap; font-size: 15px; }',
|
||
'.lsc-root .lsc-table tbody td { padding: 10px 12px; border-bottom: 1px solid #f0f2f4; vertical-align: middle; text-align: center !important; }',
|
||
'.lsc-root .lsc-table tbody td .cbi-button { margin: 2px 3px; }',
|
||
'.lsc-root .lsc-table tbody tr:last-child td { border-bottom: none; }',
|
||
'.lsc-root .lsc-table tbody tr:nth-child(even) td { background: #fafbfc; }',
|
||
'.lsc-root .lsc-table tbody tr:hover td { background: #f0f6ff; }',
|
||
'.lsc-root .lsc-badge { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 13px; font-weight: 500; line-height: 1.6; white-space: nowrap; }',
|
||
'.lsc-root .lsc-badge-ok { background: #e6f6ec; color: #18794e; }',
|
||
'.lsc-root .lsc-badge-warn { background: #fff3e0; color: #a35a00; }',
|
||
'.lsc-root .lsc-badge-err { background: #fdecec; color: #b3261e; }',
|
||
'.lsc-root .lsc-badge-off { background: #eef0f2; color: #6e7781; }',
|
||
'.lsc-root .lsc-badge-main { background: #eef4fb; color: #34618e; }',
|
||
'.lsc-root .lsc-card { border: 1px solid #e4e7ec; border-radius: 10px; padding: 14px 16px; background: #fff; box-shadow: 0 1px 3px rgba(16,24,40,.06); margin: 10px 0; }',
|
||
'.lsc-root .lsc-card h3 { margin: 0 0 8px; font-size: 15px; font-weight: 600; }',
|
||
'.lsc-root .lsc-card h4 { margin: 10px 0 4px; font-size: 13.5px; color: #57606a; }',
|
||
'.lsc-root code { background: #f6f8fa; border: 1px solid #eef0f2; border-radius: 4px; padding: 1px 6px; font-size: 1em; word-break: break-all; }',
|
||
'.lsc-root .cbi-button { border-radius: 6px; transition: filter .15s, box-shadow .15s, background .15s; text-transform: none; }',
|
||
'.lsc-root .cbi-button:hover { filter: brightness(1.06); }',
|
||
'.lsc-root h2, .lsc-root h3, .lsc-root h4, .lsc-root th, .lsc-root .lsc-badge, .lsc-root code, .lsc-root .lsc-muted, .lsc-root .cbi-section-descr, .lsc-root .lsc-card * { text-transform: none !important; }',
|
||
'.lsc-root .lsc-muted { color: #777; }',
|
||
'.lsc-root .cbi-page-actions { float: none !important; text-align: center !important; }'
|
||
].join('\n');
|
||
|
||
return view.extend({
|
||
ensureStyle: function() {
|
||
if (document.getElementById('luci-app-sysctl-style') != null)
|
||
return;
|
||
|
||
document.head.appendChild(E('style', { 'id': 'luci-app-sysctl-style' }, [ LSC_CSS ]));
|
||
},
|
||
|
||
load: function() {
|
||
return Promise.all([ callList(), callStatus(), callPresetStatus() ]);
|
||
},
|
||
|
||
render: function(data) {
|
||
this.ensureStyle();
|
||
|
||
var list = (data[0] != null) ? data[0] : {};
|
||
var status = (data[1] != null) ? data[1] : {};
|
||
var preset = (data[2] != null) ? data[2] : {};
|
||
|
||
this.customEntries = list.custom || [];
|
||
this.mainEntries = list.main || [];
|
||
this.statusData = status;
|
||
this.presetData = preset;
|
||
this.browsePrefix = '';
|
||
this.searchTimer = null;
|
||
this.editing = null;
|
||
this.fileViewPath = null;
|
||
this.fileEditing = null;
|
||
this._armTimers = {};
|
||
|
||
var root = E('div', { 'class': 'cbi-map lsc-root' }, [
|
||
E('h2', {}, _('内核参数(Sysctl)管理')),
|
||
E('div', { 'class': 'cbi-map-descr' }, _(
|
||
'自定义参数保存在 %s,开机时由系统自动加载,也可随时点击“应用配置”立即生效。').format('/etc/sysctl.d/99-luci-sysctl.conf')),
|
||
this.renderCustomSection(),
|
||
this.renderPresetSection(),
|
||
this.renderBrowseSection()
|
||
]);
|
||
|
||
this.refreshCustomTable();
|
||
this.refreshSourceChips();
|
||
this.loadBrowse('');
|
||
|
||
return root;
|
||
},
|
||
|
||
/* ---------- shared helpers ---------- */
|
||
|
||
errorBox: function(msg, hint) {
|
||
var children = [ E('p', { 'style': 'margin:2px 0' }, msg) ];
|
||
|
||
if (hint)
|
||
children.push(E('p', { 'style': 'margin:2px 0;color:#666' }, hint));
|
||
|
||
return E('div', { 'class': 'alert-message error', 'style': 'margin:6px 0' }, children);
|
||
},
|
||
|
||
backendErrorHint: function(msg) {
|
||
if (/not found/i.test(msg || ''))
|
||
return _('后端缺少该方法,通常是 rpcd 尚未重新加载插件所致。请在 SSH 执行:%s 后刷新页面。').format('/etc/init.d/rpcd restart');
|
||
|
||
return _('可在 SSH 用 %s 验证后端。').format('ubus call luci.sysctl status');
|
||
},
|
||
|
||
/* Two-step inline confirmation: first click arms the button,
|
||
* second click (within 4s) runs the action. */
|
||
armButton: function(btn, confirmText, action) {
|
||
if (btn.getAttribute('data-armed') == '1') {
|
||
btn.setAttribute('data-armed', '0');
|
||
btn.textContent = btn.getAttribute('data-orig');
|
||
action();
|
||
return;
|
||
}
|
||
|
||
btn.setAttribute('data-armed', '1');
|
||
btn.setAttribute('data-orig', btn.textContent);
|
||
btn.textContent = confirmText;
|
||
|
||
this._armTimers[btn.getAttribute('data-arm-id')] = window.setTimeout(function() {
|
||
if (btn.getAttribute('data-armed') == '1') {
|
||
btn.setAttribute('data-armed', '0');
|
||
btn.textContent = btn.getAttribute('data-orig');
|
||
}
|
||
}, 4000);
|
||
},
|
||
|
||
nextArmId: function() {
|
||
this._armSeq = (this._armSeq || 0) + 1;
|
||
return String(this._armSeq);
|
||
},
|
||
|
||
/* ---------- section 1: custom parameters ---------- */
|
||
|
||
renderCustomSection: function() {
|
||
this.customTableBody = E('tbody', {});
|
||
this.tableWrapBox = E('div', { 'class': 'lsc-tablewrap' });
|
||
this.customEmptyBox = E('div', {
|
||
'style': 'display:none;text-align:center;color:#777;padding:10px 0'
|
||
}, _('暂无自定义参数,点击下方“添加参数”开始。'));
|
||
this.editFormBox = E('div', { 'style': 'display:none' });
|
||
this.fileViewBox = E('div', { 'style': 'display:none' });
|
||
this.applyResultBox = E('div', { 'style': 'display:none' });
|
||
this.sourceChipsBox = E('div', { 'class': 'lsc-chips' });
|
||
|
||
var descr = [
|
||
E('p', { 'style': 'margin:4px 0' }, [
|
||
_('配置源(点击文件名可在下方查看/编辑,按顺序应用,后面的文件覆盖前面的同名参数):'),
|
||
this.sourceChipsBox
|
||
])
|
||
];
|
||
|
||
var table = E('table', { 'class': 'lsc-table' }, [
|
||
E('thead', {}, E('tr', {}, [
|
||
E('th', {}, _('参数名')),
|
||
E('th', {}, _('配置值')),
|
||
E('th', {}, _('当前值')),
|
||
E('th', {}, _('状态')),
|
||
E('th', {}, _('操作'))
|
||
])),
|
||
this.customTableBody
|
||
]);
|
||
|
||
this.tableWrapBox.appendChild(table);
|
||
|
||
return E('div', { 'class': 'cbi-section' }, [
|
||
E('h3', {}, _('自定义参数')),
|
||
E('div', { 'class': 'cbi-section-descr' }, descr),
|
||
this.editFormBox,
|
||
this.tableWrapBox,
|
||
this.customEmptyBox,
|
||
this.fileViewBox,
|
||
this.applyResultBox,
|
||
E('div', { 'class': 'cbi-page-actions' }, [
|
||
E('button', {
|
||
'class': 'cbi-button cbi-button-add',
|
||
'click': ui.createHandlerFn(this, 'showEditForm', null)
|
||
}, _('添加参数')),
|
||
' ',
|
||
E('button', {
|
||
'class': 'cbi-button cbi-button-apply',
|
||
'click': ui.createHandlerFn(this, 'applyConfig')
|
||
}, _('应用配置')),
|
||
' ',
|
||
E('button', {
|
||
'class': 'cbi-button cbi-button-negative',
|
||
'click': ui.createHandlerFn(this, 'reloadList')
|
||
}, _('刷新'))
|
||
])
|
||
]);
|
||
},
|
||
|
||
refreshCustomTable: function() {
|
||
var rows = [];
|
||
var i, e;
|
||
|
||
/* Main-config entries (/etc/sysctl.conf) are intentionally NOT listed
|
||
* here anymore: they are reachable through the source chips above,
|
||
* showing them twice was redundant. */
|
||
|
||
if (this.customEntries.length == 0) {
|
||
/* hide the whole (empty) table incl. header, show a one-liner */
|
||
this.tableWrapBox.style.display = 'none';
|
||
this.customEmptyBox.style.display = '';
|
||
dom.content(this.customTableBody, rows);
|
||
return;
|
||
}
|
||
|
||
this.tableWrapBox.style.display = '';
|
||
this.customEmptyBox.style.display = 'none';
|
||
|
||
for (i = 0; i < this.customEntries.length; i++) {
|
||
e = this.customEntries[i];
|
||
|
||
var badgeEl;
|
||
|
||
if (e.disabled)
|
||
badgeEl = badge(_('已禁用'), 'off');
|
||
else if (!e.exists)
|
||
badgeEl = badge(_('无效参数'), 'err');
|
||
else if (e.match)
|
||
badgeEl = badge(_('已生效'), 'ok');
|
||
else
|
||
badgeEl = badge(_('未应用'), 'warn');
|
||
|
||
var delBtn = E('button', { 'class': 'cbi-button cbi-button-remove' }, _('删除'));
|
||
|
||
rows.push(E('tr', {}, [
|
||
E('td', {}, [ E('code', {}, e.key) ]),
|
||
E('td', {}, [ E('code', {}, e.value) ]),
|
||
E('td', {}, [ E('code', {}, (e.current == null) ? '—' : e.current) ]),
|
||
E('td', {}, badgeEl),
|
||
E('td', { 'style': 'white-space:nowrap' }, [
|
||
E('button', {
|
||
'class': 'cbi-button cbi-button-edit',
|
||
'click': ui.createHandlerFn(this, 'showEditForm', { isNew: false, entry: e })
|
||
}, _('编辑')),
|
||
' ',
|
||
e.disabled
|
||
? E('button', { 'class': 'cbi-button cbi-button-apply', 'click': ui.createHandlerFn(this, 'toggleEntry', e, false) }, _('启用'))
|
||
: E('button', { 'class': 'cbi-button cbi-button-remove', 'click': ui.createHandlerFn(this, 'toggleEntry', e, true) }, _('禁用')),
|
||
' ',
|
||
delBtn
|
||
])
|
||
]));
|
||
|
||
(function(view, entry, btn) {
|
||
btn.addEventListener('click', function() {
|
||
view.armButton(btn, _('确认删除?'), function() { view.removeEntry(entry); });
|
||
});
|
||
})(this, e, delBtn);
|
||
}
|
||
|
||
dom.content(this.customTableBody, rows);
|
||
},
|
||
|
||
showEditForm: function(entry) {
|
||
this.editing = (entry != null) ? entry : null;
|
||
this.editFromBrowse = (entry != null && entry.fromBrowse === true);
|
||
this.renderEditForm();
|
||
|
||
if (this.editing != null && this.editFormBox.scrollIntoView)
|
||
this.editFormBox.scrollIntoView({ block: 'nearest' });
|
||
},
|
||
|
||
renderEditForm: function() {
|
||
var self = this;
|
||
var st = this.editing;
|
||
|
||
if (st == null) {
|
||
this.editFormBox.style.display = 'none';
|
||
dom.content(this.editFormBox, []);
|
||
return;
|
||
}
|
||
|
||
this.editFormBox.style.display = '';
|
||
|
||
var isNew = (st.isNew === true);
|
||
var key = (st.entry != null) ? st.entry.key : (st.key || '');
|
||
|
||
var keyInput = E('input', {
|
||
'type': 'text', 'class': 'cbi-input-text', 'style': 'width:100%',
|
||
'placeholder': 'net.ipv4.tcp_fastopen', 'value': key
|
||
});
|
||
|
||
var valInput = E('input', {
|
||
'type': 'text', 'class': 'cbi-input-text', 'style': 'width:100%',
|
||
'placeholder': '3', 'value': (st.entry != null) ? st.entry.value : (st.value || '')
|
||
});
|
||
|
||
var applyChk = E('input', { 'type': 'checkbox' });
|
||
applyChk.checked = true;
|
||
|
||
var disabledChk = E('input', { 'type': 'checkbox' });
|
||
disabledChk.checked = (!isNew && st.entry.disabled == true);
|
||
|
||
var errBox = this.errorBox('', '');
|
||
errBox.style.display = 'none';
|
||
|
||
var title = isNew
|
||
? (st.fromMain ? _('自定义覆盖主配置:%s').format(key) : _('添加内核参数'))
|
||
: _('编辑内核参数:%s').format(key);
|
||
|
||
var note = isNew
|
||
? (st.fromMain
|
||
? _('将把该参数写入自定义配置以覆盖主配置取值,主配置文件本身保持不变。')
|
||
: _('若参数在当前内核中不存在(例如模块未加载),仍可保存,但会标记为无效。'))
|
||
: _('修改参数名保存后将替换原条目(先写入新参数名,再删除旧参数名)。');
|
||
|
||
var saveBtn = E('button', { 'class': 'btn cbi-button-positive important' }, _('保存'));
|
||
|
||
saveBtn.addEventListener('click', function() { self.saveEditForm(keyInput, valInput, applyChk, disabledChk, errBox); });
|
||
|
||
dom.content(this.editFormBox, [
|
||
E('div', { 'class': 'lsc-card' }, [
|
||
E('strong', {}, title),
|
||
E('div', { 'class': 'cbi-value', 'style': 'margin-top:8px' }, [
|
||
E('label', { 'class': 'cbi-value-title' }, _('参数名')),
|
||
E('div', { 'class': 'cbi-value-field' }, keyInput)
|
||
]),
|
||
E('div', { 'class': 'cbi-value' }, [
|
||
E('label', { 'class': 'cbi-value-title' }, _('值')),
|
||
E('div', { 'class': 'cbi-value-field' }, valInput)
|
||
]),
|
||
E('div', { 'class': 'cbi-value' }, [
|
||
E('label', { 'class': 'cbi-value-title' }, ' '),
|
||
E('div', { 'class': 'cbi-value-field' }, [
|
||
E('label', { 'style': 'display:inline-flex;align-items:center;gap:6px' }, [ applyChk, _('立即写入内核(运行时生效)') ]),
|
||
E('label', { 'style': 'display:inline-flex;align-items:center;gap:6px;margin-left:16px' }, [ disabledChk, _('禁用该条目(写入配置但注释掉)') ])
|
||
])
|
||
]),
|
||
E('div', { 'style': 'color:#666;margin-top:4px' }, note),
|
||
errBox,
|
||
E('div', { 'style': 'margin-top:8px' }, [
|
||
saveBtn, ' ',
|
||
E('button', { 'class': 'btn', 'click': ui.createHandlerFn(self, 'hideEditForm') }, _('取消'))
|
||
])
|
||
])
|
||
]);
|
||
},
|
||
|
||
hideEditForm: function() {
|
||
this.editing = null;
|
||
this.renderEditForm();
|
||
|
||
/* edit was started from the browse section: scroll back there so the
|
||
* user does not get lost at the top of the page */
|
||
if (this.editFromBrowse) {
|
||
this.editFromBrowse = false;
|
||
|
||
if (this.browseSectionBox != null && this.browseSectionBox.scrollIntoView)
|
||
this.browseSectionBox.scrollIntoView({ block: 'start' });
|
||
}
|
||
},
|
||
|
||
saveEditForm: function(keyInput, valInput, applyChk, disabledChk, errBox) {
|
||
var self = this;
|
||
var st = this.editing;
|
||
|
||
if (st == null)
|
||
return;
|
||
|
||
var isNew = (st.isNew === true);
|
||
var origKey = (st.entry != null) ? st.entry.key : null;
|
||
var key = (keyInput.value || '').trim();
|
||
var val = (valInput.value || '').trim();
|
||
var disabled = disabledChk.checked;
|
||
var applyNow = applyChk.checked && !disabled;
|
||
|
||
errBox.style.display = 'none';
|
||
|
||
if (!KEY_RE.test(key)) {
|
||
errBox.textContent = '';
|
||
errBox.appendChild(E('p', {}, _('参数名格式无效,仅允许字母、数字、点、下划线和连字符。')));
|
||
errBox.style.display = '';
|
||
return;
|
||
}
|
||
|
||
if (val == '') {
|
||
errBox.textContent = '';
|
||
errBox.appendChild(E('p', {}, _('值不能为空。')));
|
||
errBox.style.display = '';
|
||
return;
|
||
}
|
||
|
||
var p = callSet(key, val, disabled, applyNow);
|
||
|
||
if (!isNew && key != origKey) {
|
||
p = p.then(function(res) {
|
||
if (res == null || res.code != 0)
|
||
return res;
|
||
|
||
return callRemove(origKey).then(function() {
|
||
return res;
|
||
}).catch(function() {
|
||
res.rename_leftover = origKey;
|
||
return res;
|
||
});
|
||
});
|
||
}
|
||
|
||
p.then(function(res) {
|
||
if (res == null || res.code != 0) {
|
||
errBox.textContent = '';
|
||
errBox.appendChild(self.errorBox(_('保存失败:%s').format((res != null && res.error) ? res.error : _('未知错误'))));
|
||
errBox.style.display = '';
|
||
return;
|
||
}
|
||
|
||
var notes = [];
|
||
|
||
if (res.rename_leftover)
|
||
notes.push(E('p', { 'style': 'color:#a80' }, _('旧参数 %s 删除失败,请在本页手动删除该条目。').format(res.rename_leftover)));
|
||
|
||
if (!res.exists)
|
||
notes.push(E('p', { 'style': 'color:#a80' }, _('参数 %s 在当前内核中不存在(可能模块未加载),配置已保存但不会生效。').format(key)));
|
||
else if (applyNow && res.applied === 'readonly')
|
||
notes.push(E('p', { 'style': 'color:#a80' }, _('参数 %s 为只读,无法在运行时修改(配置已保存,重启后尝试应用)。').format(key)));
|
||
else if (applyNow && res.applied === false)
|
||
notes.push(E('p', { 'style': 'color:#a80' }, _('参数 %s 已写入配置,但运行时校验不一致,请检查取值格式。').format(key)));
|
||
|
||
if (notes.length == 0)
|
||
notes.push(E('p', {}, (st.fromMain && isNew) ? _('已创建覆盖条目:%s。') : _('参数 %s 已保存。').format(key)));
|
||
|
||
self.hideEditForm();
|
||
self.reloadList();
|
||
|
||
dom.content(self.applyResultBox, [ E('div', { 'class': 'alert-message', 'style': 'margin:6px 0' }, notes) ]);
|
||
self.applyResultBox.style.display = '';
|
||
}).catch(function(e) {
|
||
errBox.textContent = '';
|
||
errBox.appendChild(self.errorBox(_('保存失败:%s').format(e.message), self.backendErrorHint(e.message)));
|
||
errBox.style.display = '';
|
||
});
|
||
},
|
||
|
||
reloadList: function() {
|
||
var self = this;
|
||
|
||
return Promise.all([ callList(), callStatus(), callPresetStatus() ]).then(function(data) {
|
||
var list = (data[0] != null) ? data[0] : {};
|
||
|
||
self.customEntries = list.custom || [];
|
||
self.mainEntries = list.main || [];
|
||
self.statusData = (data[1] != null) ? data[1] : {};
|
||
self.presetData = (data[2] != null) ? data[2] : {};
|
||
self.refreshCustomTable();
|
||
self.refreshPresetStatus();
|
||
self.refreshSourceChips();
|
||
}).catch(function(e) {
|
||
ui.addNotification(null, E('p', {}, _('加载失败:%s').format(e.message)), 'error');
|
||
});
|
||
},
|
||
|
||
refreshSourceChips: function() {
|
||
var self = this;
|
||
var files = (this.statusData != null && this.statusData.files != null) ? this.statusData.files : [];
|
||
|
||
if (this.sourceChipsBox == null)
|
||
return;
|
||
|
||
var chips = [];
|
||
|
||
/* Plugin-managed files (98-online-preset.conf, 99-luci-sysctl.conf)
|
||
* are managed through their dedicated sections; opening them via the
|
||
* file panel is refused by the backend, so hide their chips. */
|
||
var pluginManaged = {
|
||
'/etc/sysctl.d/99-luci-sysctl.conf': true,
|
||
'/etc/sysctl.d/98-online-preset.conf': true
|
||
};
|
||
|
||
for (var i = 0; i < files.length; i++) {
|
||
var f = files[i];
|
||
|
||
if (pluginManaged[f.path])
|
||
continue;
|
||
|
||
chips.push(E('button', {
|
||
'class': 'lsc-chip' + (this.fileViewPath == f.path ? ' lsc-chip-active' : ''),
|
||
'title': _('点击查看并可编辑该文件内的参数'),
|
||
'click': ui.createHandlerFn(self, 'showFileView', f.path)
|
||
}, '%s (%d)'.format(f.path.replace('/etc/', ''), f.count)));
|
||
}
|
||
|
||
dom.content(this.sourceChipsBox, chips);
|
||
},
|
||
|
||
toggleEntry: function(entry, disabled) {
|
||
var self = this;
|
||
|
||
return callSet(entry.key, entry.value, disabled, false).then(function(res) {
|
||
if (res == null || res.code != 0) {
|
||
ui.addNotification(null,
|
||
E('p', {}, _('操作失败:%s').format((res != null && res.error) ? res.error : 'unknown error')),
|
||
'error');
|
||
}
|
||
|
||
self.reloadList();
|
||
}).catch(function(e) {
|
||
ui.addNotification(null, E('p', {}, _('操作失败:%s').format(e.message)), 'error');
|
||
});
|
||
},
|
||
|
||
removeEntry: function(entry) {
|
||
var self = this;
|
||
|
||
return callRemove(entry.key).then(function(res) {
|
||
if (res == null || res.code != 0) {
|
||
ui.addNotification(null,
|
||
E('p', {}, _('删除失败:%s').format((res != null && res.error) ? res.error : 'unknown error')),
|
||
'error');
|
||
}
|
||
|
||
self.reloadList();
|
||
}).catch(function(e) {
|
||
ui.addNotification(null, E('p', {}, _('删除失败:%s').format(e.message)), 'error');
|
||
});
|
||
},
|
||
|
||
applyConfig: function() {
|
||
var self = this;
|
||
|
||
dom.content(this.applyResultBox, [ E('p', { 'style': 'color:#777;padding:6px' }, _('正在应用全部配置,请稍候…')) ]);
|
||
this.applyResultBox.style.display = '';
|
||
|
||
return callApply().then(function(res) {
|
||
var errors = (res != null && res.errors != null) ? res.errors : [];
|
||
var content;
|
||
|
||
if (res == null || res.code != 0) {
|
||
content = self.errorBox(_('应用失败:%s').format((res != null && res.error) ? res.error : 'unknown error'));
|
||
}
|
||
else if (errors.length == 0) {
|
||
content = E('div', { 'class': 'alert-message', 'style': 'margin:6px 0' }, [
|
||
E('p', {}, _('全部配置已成功应用。'))
|
||
]);
|
||
}
|
||
else {
|
||
var items = [];
|
||
|
||
for (var i = 0; i < errors.length; i++)
|
||
items.push(E('li', {}, [
|
||
E('strong', {}, errors[i].file),
|
||
E('pre', { 'style': 'white-space:pre-wrap;margin:4px 0' }, errors[i].output || '')
|
||
]));
|
||
|
||
content = E('div', { 'class': 'alert-message warning', 'style': 'margin:6px 0' }, [
|
||
E('p', {}, _('应用完成,以下文件存在报错(常见原因:参数不存在、只读或权限不足):')),
|
||
E('ul', {}, items)
|
||
]);
|
||
}
|
||
|
||
dom.content(self.applyResultBox, [ content ]);
|
||
self.reloadList();
|
||
}).catch(function(e) {
|
||
dom.content(self.applyResultBox, [ self.errorBox(_('应用失败:%s').format(e.message), self.backendErrorHint(e.message)) ]);
|
||
});
|
||
},
|
||
|
||
/* ---------- per-file viewing / editing (inline) ---------- */
|
||
|
||
showFileView: function(path) {
|
||
this.fileViewPath = (this.fileViewPath == path) ? null : path;
|
||
this.fileEditing = null;
|
||
this.refreshSourceChips();
|
||
this.renderFilePanel();
|
||
},
|
||
|
||
renderFilePanel: function() {
|
||
var self = this;
|
||
var path = this.fileViewPath;
|
||
var box = this.fileViewBox;
|
||
|
||
if (path == null) {
|
||
box.style.display = 'none';
|
||
dom.content(box, []);
|
||
return;
|
||
}
|
||
|
||
box.style.display = '';
|
||
dom.content(box, [
|
||
E('h3', { 'style': 'text-align:center' }, _('文件内容:%s').format(path)),
|
||
E('p', { 'style': 'color:#777;text-align:center' }, _('加载中…'))
|
||
]);
|
||
|
||
callFileView(path).then(function(res) {
|
||
if (self.fileViewPath != path)
|
||
return;
|
||
|
||
if (res == null || res.code != 0) {
|
||
dom.content(box, [
|
||
E('h3', { 'style': 'text-align:center' }, _('文件内容:%s').format(path)),
|
||
self.errorBox((res != null && res.error) ? res.error : _('未知错误'), self.backendErrorHint(res != null ? res.error : ''))
|
||
]);
|
||
return;
|
||
}
|
||
|
||
var rows = [];
|
||
|
||
for (var i = 0; i < (res.entries || []).length; i++) {
|
||
var e = res.entries[i];
|
||
var editing = (self.fileEditing != null && self.fileEditing.key == e.key);
|
||
|
||
if (editing) {
|
||
rows.push(self.renderFileEditRow(res.path, e));
|
||
continue;
|
||
}
|
||
|
||
var ops;
|
||
|
||
if (res.editable) {
|
||
var delBtn = E('button', { 'class': 'cbi-button cbi-button-remove' }, _('删除'));
|
||
|
||
ops = [
|
||
E('button', { 'class': 'cbi-button cbi-button-edit', 'click': ui.createHandlerFn(self, 'editFileEntry', path, e) }, _('编辑')),
|
||
' ',
|
||
e.disabled
|
||
? E('button', { 'class': 'cbi-button cbi-button-apply', 'click': ui.createHandlerFn(self, 'fileToggle', path, e, false) }, _('启用'))
|
||
: E('button', { 'class': 'cbi-button cbi-button-remove', 'click': ui.createHandlerFn(self, 'fileToggle', path, e, true) }, _('禁用')),
|
||
' ',
|
||
delBtn
|
||
];
|
||
|
||
(function(view, p, entry, btn) {
|
||
btn.addEventListener('click', function() {
|
||
view.armButton(btn, _('确认删除?'), function() { view.fileDelete(p, entry); });
|
||
});
|
||
})(self, res.path, e, delBtn);
|
||
}
|
||
else {
|
||
ops = [ E('button', {
|
||
'class': 'cbi-button cbi-button-add',
|
||
'click': ui.createHandlerFn(self, 'showEditForm', { isNew: true, key: e.key, value: e.value, fromMain: true })
|
||
}, _('自定义')) ];
|
||
}
|
||
|
||
rows.push(E('tr', {}, [
|
||
E('td', {}, [ E('code', {}, (e.disabled ? '# ' : '') + e.key) ]),
|
||
E('td', {}, [ E('code', {}, e.value) ]),
|
||
E('td', {}, [ E('code', {}, (e.disabled || e.current == null) ? '—' : e.current) ]),
|
||
E('td', { 'style': 'white-space:nowrap' }, ops)
|
||
]));
|
||
}
|
||
|
||
if (rows.length == 0)
|
||
rows.push(E('tr', {}, E('td', { 'colspan': 4, 'style': 'text-align:center;color:#777;padding:12px' },
|
||
_('(空文件)'))));
|
||
|
||
var note = res.editable
|
||
? _('此文件中的参数可直接编辑/禁用/删除,保存即写入 %s;点击"应用配置"后对内核生效。').format(res.path)
|
||
: _('此文件由插件管理,不支持直接编辑:在线预设请在下方"在线预设"区更新,自定义参数请在上方表格中管理。');
|
||
|
||
dom.content(box, [
|
||
E('h3', { 'style': 'text-align:center' }, _('文件内容:%s').format(res.path)),
|
||
E('p', { 'style': 'margin:4px 0;color:#666;text-align:center' }, note),
|
||
E('div', { 'class': 'lsc-tablewrap' }, E('table', { 'class': 'lsc-table' }, [
|
||
E('thead', {}, E('tr', {}, [
|
||
E('th', {}, _('参数')),
|
||
E('th', {}, _('配置值')),
|
||
E('th', {}, _('当前值')),
|
||
E('th', {}, _('操作'))
|
||
])),
|
||
E('tbody', rows)
|
||
])),
|
||
E('div', { 'style': 'margin-top:8px;text-align:center' }, [
|
||
E('button', { 'class': 'btn', 'click': ui.createHandlerFn(self, 'hideFileView') }, _('收起'))
|
||
])
|
||
]);
|
||
}).catch(function(e) {
|
||
if (self.fileViewPath != path)
|
||
return;
|
||
|
||
dom.content(box, [
|
||
E('h3', { 'style': 'text-align:center' }, _('文件内容:%s').format(path)),
|
||
self.errorBox(_('读取失败:%s').format(e.message), self.backendErrorHint(e.message))
|
||
]);
|
||
});
|
||
},
|
||
|
||
renderFileEditRow: function(path, entry) {
|
||
var self = this;
|
||
|
||
var valInput = E('input', {
|
||
'type': 'text', 'class': 'cbi-input-text', 'style': 'width:95%', 'value': entry.value
|
||
});
|
||
|
||
var disabledChk = E('input', { 'type': 'checkbox' });
|
||
disabledChk.checked = (entry.disabled == true);
|
||
|
||
var saveBtn = E('button', { 'class': 'cbi-button cbi-button-positive important' }, _('保存'));
|
||
|
||
saveBtn.addEventListener('click', function() {
|
||
var val = (valInput.value || '').trim();
|
||
|
||
if (val == '') {
|
||
alert(_('值不能为空。'));
|
||
return;
|
||
}
|
||
|
||
callFileSet(path, entry.key, val, disabledChk.checked).then(function(res) {
|
||
if (res == null || res.code != 0) {
|
||
alert(_('保存失败:%s').format((res != null && res.error) ? res.error : _('未知错误')));
|
||
return;
|
||
}
|
||
|
||
self.fileEditing = null;
|
||
self.renderFilePanel();
|
||
self.reloadList();
|
||
}).catch(function(e) {
|
||
alert(_('保存失败:%s').format(e.message));
|
||
});
|
||
});
|
||
|
||
return E('tr', { 'style': 'background:#fafafa' }, [
|
||
E('td', {}, [ E('code', {}, entry.key) ]),
|
||
E('td', { 'colspan': 2 }, [
|
||
valInput, ' ',
|
||
E('label', { 'style': 'display:inline-flex;align-items:center;gap:4px' }, [ disabledChk, _('禁用') ])
|
||
]),
|
||
E('td', { 'style': 'white-space:nowrap' }, [
|
||
saveBtn, ' ',
|
||
E('button', { 'class': 'btn', 'click': ui.createHandlerFn(self, 'cancelFileEdit') }, _('取消'))
|
||
])
|
||
]);
|
||
},
|
||
|
||
editFileEntry: function(path, entry) {
|
||
this.fileEditing = entry;
|
||
this.renderFilePanel();
|
||
},
|
||
|
||
cancelFileEdit: function() {
|
||
this.fileEditing = null;
|
||
this.renderFilePanel();
|
||
},
|
||
|
||
fileToggle: function(path, entry, disabled) {
|
||
var self = this;
|
||
|
||
return callFileSet(path, entry.key, entry.value, disabled).then(function(res) {
|
||
if (res == null || res.code != 0) {
|
||
ui.addNotification(null,
|
||
E('p', {}, _('操作失败:%s').format((res != null && res.error) ? res.error : _('未知错误'))),
|
||
'error');
|
||
}
|
||
|
||
self.renderFilePanel();
|
||
self.reloadList();
|
||
}).catch(function(e) {
|
||
ui.addNotification(null, E('p', {}, _('操作失败:%s').format(e.message)), 'error');
|
||
});
|
||
},
|
||
|
||
fileDelete: function(path, entry) {
|
||
var self = this;
|
||
|
||
return callFileDelete(path, entry.key).then(function(res) {
|
||
if (res == null || res.code != 0) {
|
||
ui.addNotification(null,
|
||
E('p', {}, _('删除失败:%s').format((res != null && res.error) ? res.error : _('未知错误'))),
|
||
'error');
|
||
}
|
||
|
||
self.renderFilePanel();
|
||
self.reloadList();
|
||
}).catch(function(e) {
|
||
ui.addNotification(null, E('p', {}, _('删除失败:%s').format(e.message)), 'error');
|
||
});
|
||
},
|
||
|
||
hideFileView: function() {
|
||
this.fileViewPath = null;
|
||
this.fileEditing = null;
|
||
this.refreshSourceChips();
|
||
this.renderFilePanel();
|
||
},
|
||
|
||
/* ---------- section 2: online preset ---------- */
|
||
|
||
renderPresetSection: function() {
|
||
var self = this;
|
||
|
||
this.presetStatusLine = E('div', { 'style': 'margin:6px 0;padding:6px 10px;background:#f5f5f5;border-radius:3px' });
|
||
this.presetResultBox = E('div', { 'style': 'display:none' });
|
||
|
||
this.presetUrlInput = E('input', {
|
||
'type': 'text', 'class': 'cbi-input-text', 'style': 'width:100%',
|
||
'placeholder': 'https://github.com/user/repo/blob/main/sysctl.conf(也支持 raw 地址)'
|
||
});
|
||
|
||
this.presetPrefixInput = E('input', {
|
||
'type': 'text', 'class': 'cbi-input-text', 'style': 'width:260px',
|
||
'placeholder': 'https://gh-proxy.org/'
|
||
});
|
||
|
||
var delBtn = E('button', { 'class': 'cbi-button cbi-button-remove' }, _('移除预设'));
|
||
|
||
delBtn.addEventListener('click', function() {
|
||
self.armButton(delBtn, _('确认移除?'), function() { self.presetRemove(); });
|
||
});
|
||
|
||
return E('div', { 'class': 'cbi-section' }, [
|
||
E('h3', {}, _('在线预设')),
|
||
E('div', { 'class': 'cbi-section-descr' }, [
|
||
E('p', { 'style': 'margin:4px 0' }, _(
|
||
'从在线源(GitHub 等)获取现成的 sysctl 优化配置,预览确认后一键导入。')),
|
||
E('p', { 'style': 'margin:4px 0' }, _(
|
||
'预设保存于 %s,由本插件整体管理(更新时全量替换)。如需覆盖个别条目,请在上方"自定义参数"中添加,其加载顺序更靠后、优先级更高。').format('/etc/sysctl.d/98-online-preset.conf'))
|
||
]),
|
||
this.presetStatusLine,
|
||
E('div', { 'class': 'cbi-value' }, [
|
||
E('label', { 'class': 'cbi-value-title' }, _('配置源 URL')),
|
||
E('div', { 'class': 'cbi-value-field' }, this.presetUrlInput)
|
||
]),
|
||
E('div', { 'class': 'cbi-value' }, [
|
||
E('label', { 'class': 'cbi-value-title' }, _('镜像前缀(可选)')),
|
||
E('div', { 'class': 'cbi-value-field' }, [
|
||
this.presetPrefixInput,
|
||
E('div', { 'style': 'color:#777;margin-top:2px' },
|
||
_('直连 GitHub 不可用时填写镜像前缀,最终地址为 前缀 + 原始 URL。'))
|
||
])
|
||
]),
|
||
E('div', { 'class': 'cbi-page-actions' }, [
|
||
E('button', { 'class': 'cbi-button cbi-button-apply', 'click': ui.createHandlerFn(this, 'presetFetchPreview') }, _('获取并预览')),
|
||
' ',
|
||
E('button', { 'class': 'cbi-button cbi-button-find', 'click': ui.createHandlerFn(this, 'presetCheckUpdates') }, _('检查更新')),
|
||
' ',
|
||
delBtn
|
||
]),
|
||
this.presetResultBox
|
||
]);
|
||
},
|
||
|
||
refreshPresetStatus: function() {
|
||
var p = this.presetData || {};
|
||
var line;
|
||
|
||
if (p.present) {
|
||
line = [ E('strong', {}, _('已导入') + ':'), _('%d 条参数').format(p.count || 0) ];
|
||
|
||
if (p.source) {
|
||
line.push(E('span', { 'style': 'color:#777' }, ' · ' + _('来源') + ' '));
|
||
line.push(E('span', { 'style': 'word-break:break-all' }, p.source));
|
||
}
|
||
|
||
if (p.fetched) {
|
||
line.push(E('span', { 'style': 'color:#777' }, ' · ' + _('更新于') + ' '));
|
||
line.push(this.formatEpoch(p.fetched));
|
||
}
|
||
}
|
||
else {
|
||
line = [ E('span', { 'style': 'color:#777' }, _('尚未导入在线预设。')) ];
|
||
}
|
||
|
||
dom.content(this.presetStatusLine, line);
|
||
},
|
||
|
||
formatEpoch: function(s) {
|
||
var n = Number(s);
|
||
|
||
if (s == null || s === '' || isNaN(n) || n <= 0)
|
||
return '—';
|
||
|
||
try {
|
||
return new Date(n * 1000).toLocaleString();
|
||
} catch (e) {
|
||
return String(s);
|
||
}
|
||
},
|
||
|
||
presetFetchPreview: function() {
|
||
var self = this;
|
||
var url = (this.presetUrlInput.value || '').trim();
|
||
|
||
if (url == '') {
|
||
this.showPresetResult(this.errorBox(_('请先填写配置源 URL。')));
|
||
return;
|
||
}
|
||
|
||
this.showPresetResult(E('p', { 'style': 'color:#777;padding:6px' }, _('正在获取配置源信息,请稍候…')));
|
||
|
||
return callPresetList(url, (this.presetPrefixInput.value || '').trim()).then(function(lres) {
|
||
if (lres != null && lres.is_dir) {
|
||
self.renderPresetDirList(lres);
|
||
return;
|
||
}
|
||
|
||
return self.fetchAndPreviewPresetFile(url);
|
||
}).catch(function(e) {
|
||
self.showPresetResult(self.errorBox(_('获取失败:%s').format(e.message), self.backendErrorHint(e.message)));
|
||
});
|
||
},
|
||
|
||
showPresetResult: function(content) {
|
||
this.presetResultBox.style.display = '';
|
||
dom.content(this.presetResultBox, [ content ]);
|
||
},
|
||
|
||
fetchAndPreviewPresetFile: function(url) {
|
||
var self = this;
|
||
var prefix = (this.presetPrefixInput.value || '').trim();
|
||
|
||
this.showPresetResult(E('p', { 'style': 'color:#777;padding:6px' }, _('正在下载并解析预设,请稍候…')));
|
||
|
||
return callPresetFetch(url, prefix).then(function(res) {
|
||
if (res == null || res.code != 0) {
|
||
self.showPresetResult(self.errorBox(
|
||
(res != null && res.error) ? res.error : _('未知错误'),
|
||
self.backendErrorHint(res != null ? res.error : '')));
|
||
return;
|
||
}
|
||
|
||
self.renderPresetPreview(res);
|
||
}).catch(function(e) {
|
||
self.showPresetResult(self.errorBox(_('获取失败:%s').format(e.message), self.backendErrorHint(e.message)));
|
||
});
|
||
},
|
||
|
||
renderPresetDirList: function(res) {
|
||
var self = this;
|
||
var rows = [];
|
||
|
||
for (var i = 0; i < (res.files || []).length; i++) {
|
||
(function(f) {
|
||
var btn = E('button', {
|
||
'class': 'lsc-chip',
|
||
'style': 'font-size:13px;padding:5px 14px'
|
||
}, f.name + (f.size > 0 ? ' · ' + (f.size / 1024).toFixed(1) + ' KB' : ''));
|
||
|
||
btn.addEventListener('click', function() { self.fetchAndPreviewPresetFile(f.url); });
|
||
rows.push(E('li', { 'style': 'margin:2px 0' }, btn));
|
||
})(res.files[i]);
|
||
}
|
||
|
||
this.presetResultBox.style.display = '';
|
||
dom.content(this.presetResultBox, [
|
||
E('h3', {}, _('预设目录')),
|
||
E('p', { 'style': 'margin:4px 0;color:#666' },
|
||
_('该地址是一个目录,发现 %d 个预设文件。点击要导入的文件(下载时同样使用镜像前缀):').format(res.files.length)),
|
||
E('ul', { 'style': 'margin:8px 0;padding-left:4px;list-style:none' }, rows),
|
||
E('div', { 'style': 'margin-top:10px' }, [
|
||
E('button', { 'class': 'btn', 'click': ui.createHandlerFn(this, 'hidePresetResult') }, _('收起'))
|
||
])
|
||
]);
|
||
},
|
||
|
||
renderPresetPreview: function(res) {
|
||
var self = this;
|
||
var rows = [];
|
||
var notes = [];
|
||
var i;
|
||
|
||
var cmap = {};
|
||
|
||
for (i = 0; i < (res.conflicts || []).length; i++)
|
||
cmap[res.conflicts[i].key] = res.conflicts[i].file;
|
||
|
||
for (i = 0; i < (res.params || []).length; i++) {
|
||
var it = res.params[i];
|
||
var conflictFile = cmap[it.key] || '';
|
||
|
||
rows.push(E('tr', {}, [
|
||
E('td', {}, [ E('code', {}, it.key) ]),
|
||
E('td', {}, [ E('code', {}, it.value) ]),
|
||
E('td', { 'style': 'color:#a80' }, conflictFile ? _('覆盖本地 %s').format(conflictFile.replace('/etc/', '')) : '')
|
||
]));
|
||
}
|
||
|
||
notes.push(E('p', {}, _('共解析出 %d 条参数,来源:%s。').format(res.params.length, res.url)));
|
||
|
||
if ((res.conflicts || []).length > 0)
|
||
notes.push(E('p', { 'class': 'alert-message warning', 'style': 'margin:6px 0' },
|
||
_('%d 条参数与本地现有配置同名,导入后将覆盖其取值。').format(res.conflicts.length)));
|
||
|
||
if ((res.invalid || []).length > 0)
|
||
notes.push(E('p', { 'class': 'alert-message warning', 'style': 'margin:6px 0' },
|
||
_('%d 行无法解析已忽略(常见为说明文字或格式错误的行)。').format(res.invalid.length)));
|
||
|
||
var importBtn = E('button', { 'class': 'btn cbi-button-positive important' }, _('导入到路由器'));
|
||
|
||
importBtn.addEventListener('click', function() { self.presetImport(res.url); });
|
||
|
||
dom.content(this.presetResultBox, [
|
||
E('h3', {}, _('预设预览')),
|
||
E('div', {}, notes),
|
||
E('div', { 'style': 'max-height:320px;overflow:auto' }, E('div', { 'class': 'lsc-tablewrap' }, E('table', { 'class': 'lsc-table' }, [
|
||
E('thead', {}, E('tr', {}, [ E('th', {}, _('参数')), E('th', {}, _('值')), E('th', {}, _('冲突')) ])),
|
||
E('tbody', rows)
|
||
]))),
|
||
E('div', { 'style': 'margin-top:10px' }, [
|
||
importBtn, ' ',
|
||
E('button', { 'class': 'btn', 'click': ui.createHandlerFn(this, 'hidePresetResult') }, _('取消'))
|
||
])
|
||
]);
|
||
},
|
||
|
||
presetImport: function(url) {
|
||
var self = this;
|
||
|
||
return callPresetImport(url, '').then(function(res) {
|
||
if (res == null || res.code != 0) {
|
||
dom.content(self.presetResultBox, [ self.errorBox(_('导入失败:%s').format((res != null && res.error) ? res.error : _('未知错误'))) ]);
|
||
return;
|
||
}
|
||
|
||
dom.content(self.presetResultBox, [
|
||
E('div', { 'class': 'alert-message', 'style': 'margin:6px 0' }, [
|
||
E('p', {}, _('已导入 %d 条预设参数到 %s。点击"应用配置"可立即生效。').format(res.count, res.path))
|
||
])
|
||
]);
|
||
|
||
self.reloadList();
|
||
}).catch(function(e) {
|
||
dom.content(self.presetResultBox, [ self.errorBox(_('导入失败:%s').format(e.message), self.backendErrorHint(e.message)) ]);
|
||
});
|
||
},
|
||
|
||
presetCheckUpdates: function() {
|
||
var self = this;
|
||
|
||
dom.content(this.presetResultBox, [ E('p', { 'style': 'color:#777;padding:6px' }, _('正在对比在线版本与本地预设,请稍候…')) ]);
|
||
this.presetResultBox.style.display = '';
|
||
|
||
return callPresetCheck().then(function(res) {
|
||
if (res == null || res.code != 0) {
|
||
dom.content(self.presetResultBox, [ self.errorBox(
|
||
(res != null && res.error) ? res.error : _('未知错误'),
|
||
self.backendErrorHint(res != null ? res.error : '')) ]);
|
||
return;
|
||
}
|
||
|
||
if ((res.added || []).length == 0 && (res.changed || []).length == 0 && (res.removed || []).length == 0) {
|
||
dom.content(self.presetResultBox, [
|
||
E('div', { 'class': 'alert-message', 'style': 'margin:6px 0' }, [
|
||
E('p', {}, _('在线预设已是最新,共 %d 条参数。').format(res.remote_count))
|
||
])
|
||
]);
|
||
return;
|
||
}
|
||
|
||
self.renderPresetDiff(res);
|
||
}).catch(function(e) {
|
||
dom.content(self.presetResultBox, [ self.errorBox(_('检查更新失败:%s').format(e.message), self.backendErrorHint(e.message)) ]);
|
||
});
|
||
},
|
||
|
||
renderPresetDiff: function(res) {
|
||
var self = this;
|
||
var blocks = [];
|
||
var i;
|
||
|
||
var mkTable = function(headers, rows) {
|
||
return E('div', { 'class': 'lsc-tablewrap' }, E('table', { 'class': 'lsc-table' }, [
|
||
E('thead', {}, E('tr', {}, headers.map(function(h) { return E('th', {}, h); }))),
|
||
E('tbody', rows)
|
||
]));
|
||
};
|
||
|
||
if ((res.added || []).length > 0) {
|
||
var addRows = [];
|
||
|
||
for (i = 0; i < res.added.length; i++)
|
||
addRows.push(E('tr', {}, [
|
||
E('td', {}, [ E('code', {}, res.added[i].key) ]),
|
||
E('td', {}, [ E('code', {}, res.added[i].value) ])
|
||
]));
|
||
|
||
blocks.push(E('div', {}, [
|
||
E('h4', {}, _('新增 %d 条').format(res.added.length)),
|
||
mkTable([ _('参数'), _('值') ], addRows)
|
||
]));
|
||
}
|
||
|
||
if ((res.changed || []).length > 0) {
|
||
var chgRows = [];
|
||
|
||
for (i = 0; i < res.changed.length; i++)
|
||
chgRows.push(E('tr', {}, [
|
||
E('td', {}, [ E('code', {}, res.changed[i].key) ]),
|
||
E('td', {}, [ E('code', {}, res.changed[i].from) ]),
|
||
E('td', {}, [ E('code', {}, res.changed[i].to) ])
|
||
]));
|
||
|
||
blocks.push(E('div', {}, [
|
||
E('h4', {}, _('变更 %d 条').format(res.changed.length)),
|
||
mkTable([ _('参数'), _('当前值'), _('新值') ], chgRows)
|
||
]));
|
||
}
|
||
|
||
if ((res.removed || []).length > 0) {
|
||
var rmRows = [];
|
||
|
||
for (i = 0; i < res.removed.length; i++)
|
||
rmRows.push(E('tr', {}, [
|
||
E('td', {}, [ E('code', {}, res.removed[i].key) ]),
|
||
E('td', {}, [ E('code', {}, res.removed[i].old) ])
|
||
]));
|
||
|
||
blocks.push(E('div', {}, [
|
||
E('h4', {}, _('移除 %d 条').format(res.removed.length)),
|
||
mkTable([ _('参数'), _('当前值') ], rmRows)
|
||
]));
|
||
}
|
||
|
||
var applyBtn = E('button', { 'class': 'btn cbi-button-positive important' }, _('应用更新'));
|
||
|
||
applyBtn.addEventListener('click', function() { self.presetImport(res.url); });
|
||
|
||
dom.content(this.presetResultBox, [
|
||
E('h3', {}, _('发现在线预设更新')),
|
||
E('p', {}, _('在线版本共 %d 条,本地 %d 条。更新会整体替换 %s。').format(res.remote_count, res.local_count, '/etc/sysctl.d/98-online-preset.conf')),
|
||
E('div', { 'style': 'max-height:380px;overflow:auto' }, blocks),
|
||
E('div', { 'style': 'margin-top:10px' }, [
|
||
applyBtn, ' ',
|
||
E('button', { 'class': 'btn', 'click': ui.createHandlerFn(this, 'hidePresetResult') }, _('取消'))
|
||
])
|
||
]);
|
||
},
|
||
|
||
hidePresetResult: function() {
|
||
this.presetResultBox.style.display = 'none';
|
||
dom.content(this.presetResultBox, []);
|
||
},
|
||
|
||
presetRemove: function() {
|
||
var self = this;
|
||
|
||
return callPresetRemove().then(function(res) {
|
||
if (res == null || res.code != 0) {
|
||
ui.addNotification(null,
|
||
E('p', {}, _('移除失败:%s').format((res != null && res.error) ? res.error : 'unknown error')),
|
||
'error');
|
||
}
|
||
|
||
self.reloadList();
|
||
}).catch(function(e) {
|
||
ui.addNotification(null, E('p', {}, _('移除失败:%s').format(e.message)), 'error');
|
||
});
|
||
},
|
||
|
||
/* ---------- section 3: browse / search ---------- */
|
||
|
||
renderBrowseSection: function() {
|
||
var self = this;
|
||
|
||
this.browseTableBody = E('tbody', {});
|
||
this.crumbs = E('div', { 'style': 'margin:6px 0' });
|
||
this.searchInput = E('input', {
|
||
'type': 'text',
|
||
'class': 'cbi-input-text',
|
||
'style': 'width:280px;display:inline-block',
|
||
'placeholder': _('搜索参数名或值(至少 2 个字符)')
|
||
});
|
||
|
||
this.searchInput.addEventListener('input', function() { self.onSearchInput(); });
|
||
|
||
var searchBtn = E('button', { 'class': 'cbi-button cbi-button-find', 'click': ui.createHandlerFn(this, 'doSearch') }, _('搜索'));
|
||
|
||
var table = E('table', { 'class': 'lsc-table' }, [
|
||
E('thead', {}, E('tr', {}, [
|
||
E('th', {}, _('参数')),
|
||
E('th', {}, _('当前值')),
|
||
E('th', {}, _('操作'))
|
||
])),
|
||
this.browseTableBody
|
||
]);
|
||
|
||
var box = E('div', { 'class': 'cbi-section' }, [
|
||
E('h3', {}, _('浏览内核参数')),
|
||
E('div', { 'class': 'cbi-section-descr' },
|
||
_('浏览 /proc/sys 下的实时参数。点击目录进入子级,也可直接搜索;点击“自定义”可将参数加入上方自定义列表。')),
|
||
E('div', { 'style': 'margin:8px 0' }, [ this.searchInput, ' ', searchBtn ]),
|
||
this.crumbs,
|
||
E('div', { 'class': 'lsc-tablewrap' }, table)
|
||
]);
|
||
|
||
this.browseSectionBox = box;
|
||
|
||
return box;
|
||
},
|
||
|
||
onSearchInput: function() {
|
||
var self = this;
|
||
|
||
if (this.searchTimer != null)
|
||
window.clearTimeout(this.searchTimer);
|
||
|
||
this.searchTimer = window.setTimeout(function() { self.doSearch(); }, 500);
|
||
},
|
||
|
||
doSearch: function() {
|
||
var self = this;
|
||
var q = (this.searchInput.value || '').trim();
|
||
|
||
if (this.searchTimer != null) {
|
||
window.clearTimeout(this.searchTimer);
|
||
this.searchTimer = null;
|
||
}
|
||
|
||
if (q.length < 2) {
|
||
this.browsePrefix = '';
|
||
this.loadBrowse('');
|
||
return;
|
||
}
|
||
|
||
this.browsePrefix = null;
|
||
this.crumbsUpdate();
|
||
|
||
dom.content(this.browseTableBody, E('tr', {},
|
||
E('td', { 'colspan': 3, 'style': 'text-align:center;color:#777;padding:12px' }, _('搜索中…'))));
|
||
|
||
callSearch(q, 500).then(function(res) {
|
||
var items = (res != null && res.items != null) ? res.items : [];
|
||
|
||
self.renderSearchResults(items, (res != null && res.truncated == true), q);
|
||
}).catch(function(e) {
|
||
dom.content(self.browseTableBody, E('tr', {},
|
||
E('td', { 'colspan': 3, 'class': 'alert-message error' }, _('搜索失败:%s').format(e.message))));
|
||
});
|
||
},
|
||
|
||
renderSearchResults: function(items, truncated, q) {
|
||
var rows = [];
|
||
|
||
items.sort(function(a, b) { return (a.key < b.key) ? -1 : (a.key > b.key) ? 1 : 0; });
|
||
|
||
for (var i = 0; i < items.length; i++)
|
||
rows.push(this.renderParamRow(items[i].key, items[i].value));
|
||
|
||
if (rows.length == 0)
|
||
rows.push(E('tr', {}, E('td', { 'colspan': 3, 'style': 'text-align:center;color:#777;padding:12px' },
|
||
_('没有匹配“%s”的参数。').format(q))));
|
||
|
||
if (truncated)
|
||
rows.push(E('tr', {}, E('td', { 'colspan': 3, 'style': 'color:#e80' },
|
||
_('结果过多已截断,请输入更精确的关键词。'))));
|
||
|
||
dom.content(this.browseTableBody, rows);
|
||
},
|
||
|
||
renderParamRow: function(key, value) {
|
||
var self = this;
|
||
|
||
return E('tr', { 'class': 'cbi-section-table-row' }, [
|
||
E('td', {}, [ E('code', {}, key) ]),
|
||
E('td', {}, [ E('code', {}, value) ]),
|
||
E('td', {}, E('button', {
|
||
'class': 'cbi-button cbi-button-add',
|
||
'click': ui.createHandlerFn(self, 'showEditForm', { isNew: true, key: key, value: value, fromBrowse: true })
|
||
}, _('自定义')))
|
||
]);
|
||
},
|
||
|
||
loadBrowse: function(prefix) {
|
||
var self = this;
|
||
|
||
dom.content(this.browseTableBody, E('tr', {},
|
||
E('td', { 'colspan': 3, 'style': 'text-align:center;color:#777;padding:12px' }, _('加载中…'))));
|
||
|
||
this.crumbsUpdate();
|
||
|
||
callBrowse(prefix).then(function(res) {
|
||
var items = (res != null && res.items != null) ? res.items : [];
|
||
var rows = [];
|
||
|
||
for (var i = 0; i < items.length; i++) {
|
||
var it = items[i];
|
||
|
||
if (it.type == 'group') {
|
||
rows.push(E('tr', { 'class': 'cbi-section-table-row' }, [
|
||
E('td', {}, E('a', {
|
||
'href': '#',
|
||
'click': ui.createHandlerFn(self, 'browseInto', it.prefix)
|
||
}, [ E('strong', {}, it.name + '/') ])),
|
||
E('td', {}, E('span', { 'style': 'color:#999' }, _('目录'))),
|
||
E('td', {}, E('a', {
|
||
'href': '#',
|
||
'style': 'text-decoration:none',
|
||
'click': ui.createHandlerFn(self, 'browseInto', it.prefix)
|
||
}, _('进入')))
|
||
]));
|
||
}
|
||
else {
|
||
rows.push(self.renderParamRow(it.key, it.value));
|
||
}
|
||
}
|
||
|
||
if (rows.length == 0)
|
||
rows.push(E('tr', {}, E('td', { 'colspan': 3, 'style': 'text-align:center;color:#777;padding:12px' },
|
||
_('(空)'))));
|
||
|
||
dom.content(self.browseTableBody, rows);
|
||
}).catch(function(e) {
|
||
dom.content(self.browseTableBody, E('tr', {},
|
||
E('td', { 'colspan': 3, 'class': 'alert-message error' }, _('加载失败:%s').format(e.message))));
|
||
});
|
||
},
|
||
|
||
browseInto: function(prefix) {
|
||
this.browsePrefix = prefix;
|
||
this.loadBrowse(prefix);
|
||
},
|
||
|
||
crumbsUpdate: function() {
|
||
var self = this;
|
||
var parts = [ E('a', {
|
||
'href': '#',
|
||
'click': ui.createHandlerFn(this, 'browseInto', '')
|
||
}, _('根目录')) ];
|
||
|
||
if (this.browsePrefix == null) {
|
||
parts.push(E('span', { 'style': 'color:#777' }, ' › ' + _('搜索') + ': ' + this.searchInput.value));
|
||
}
|
||
else if (this.browsePrefix != '') {
|
||
var segs = this.browsePrefix.replace(/\.$/, '').split('.');
|
||
var acc = '';
|
||
|
||
for (var i = 0; i < segs.length; i++) {
|
||
acc += segs[i] + '.';
|
||
|
||
parts.push(E('span', { 'style': 'color:#777' }, ' › '));
|
||
parts.push(E('a', {
|
||
'href': '#',
|
||
'click': ui.createHandlerFn(this, 'browseInto', acc)
|
||
}, segs[i]));
|
||
}
|
||
|
||
/* one-click step back to the parent directory */
|
||
var parentPrefix = (segs.length > 1)
|
||
? segs.slice(0, segs.length - 1).join('.') + '.'
|
||
: '';
|
||
|
||
parts.push(E('span', { 'style': 'color:#777' }, ' '));
|
||
parts.push(E('a', {
|
||
'href': '#',
|
||
'style': 'margin-left:12px',
|
||
'click': ui.createHandlerFn(this, 'browseInto', parentPrefix)
|
||
}, _('← 返回上一级')));
|
||
}
|
||
|
||
dom.content(this.crumbs, parts);
|
||
}
|
||
});
|
||
|