luci-mod-system: refresh repokeys
authorPaul Donald <redacted>
Tue, 3 Feb 2026 05:48:37 +0000 (06:48 +0100)
committerPaul Donald <redacted>
Tue, 3 Feb 2026 05:48:37 +0000 (06:48 +0100)
Remove manual UI setup and implement JSONMap.

Signed-off-by: Paul Donald <redacted>
modules/luci-mod-system/htdocs/luci-static/resources/view/system/repokeys.js

index 3a6c22ab2264acf6a413aab0e7ea362bc8fc9a38..7785ee4320a7cd598c97ec7c3db206aee8872f25 100644 (file)
@@ -1,8 +1,9 @@
 'use strict';
 'require baseclass';
-'require view';
 'require fs';
+'require form';
 'require ui';
+'require view';
 
 const APK_DIR = '/etc/apk/keys/';
 const OPKG_DIR = '/etc/opkg/keys/';
@@ -61,34 +62,6 @@ function safeText(str) {
        }[s]));
 }
 
-function renderKeyItem(pubkey) {
-       const safeFile = isFileInSafeList(pubkey?.filename);
-       const lines = pubkey?.key?.trim()?.split('\n').map(line =>
-               [ E('br'), E('code', {}, [ safeText(line) ]) ]
-       ).flat();
-       return E('div', {
-               class: 'item',
-               click: (isReadonlyView || safeFile) ? null : removeKey,
-               'data-file': pubkey?.filename,
-               'data-key': normalizeKey(pubkey?.key)
-       }, [
-               E('strong', {}, [ pubkey?.filename || _('Unnamed key') ]),
-               ...lines
-       ]);
-}
-
-function refreshKeyList(list, keys) {
-       while (!matchesElem(list.firstElementChild, '.add-item'))
-               list.removeChild(list.firstElementChild);
-
-       keys.forEach(function(pubkey) {
-               list.insertBefore(renderKeyItem(pubkey), list.lastElementChild);
-       });
-
-       if (list.firstElementChild === list.lastElementChild)
-               list.insertBefore(E('p', _('No software repository public keys present yet.')), list.lastElementChild);
-}
-
 function saveKeyFile(keyContent, file, fileContent) {
        const ts = Date.now();
        // Note: opkg can only verify against a key with filename that matches its key hash
@@ -98,23 +71,20 @@ function saveKeyFile(keyContent, file, fileContent) {
        return fs.write(KEYDIR + (filename ?? noname), fileContent ?? keyContent, 384 /* 0600 */);
 }
 
-function removeKey(ev) {
-       const file = ev.currentTarget.getAttribute('data-file');
-       const list = findParent(ev.target, '.cbi-dynlist');
-
+function removeKey(ev, key) {
        L.showModal(_('Delete key'), [
                E('div', _('Really delete the following software repository public key?')),
-               E('pre', [ file ]),
+               E('pre', [ key.filename ]),
                E('div', { class: 'right' }, [
                        E('div', { class: 'btn', click: L.hideModal }, _('Cancel')),
                        ' ',
                        E('div', {
                                class: 'btn danger',
                                click: function() {
-                                       fs.remove(KEYDIR + file).then(() => {
-                                               return listKeyFiles().then(keys => refreshKeyList(list, keys));
-                                       });
-                                       ui.hideModal();
+                                       fs.remove(KEYDIR + key.filename)
+                                               .then(() => window.location.reload())
+                                               .catch(e => ui.addNotification(null, E('p', e.message)))
+                                               .finally(() => ui.hideModal());
                                }
                        }, _('Delete key'))
                ])
@@ -138,11 +108,10 @@ function keyEnvironmentCheck(key) {
 }
 
 function addKey(ev, file, fileContent) {
-       const list = findParent(ev.target, '.cbi-dynlist');
-       const input = list.querySelector('textarea[type="text"]');
-       let key = (fileContent ?? input.value.trim());
+       const input = document.getElementById('key-input');
+       const key = (fileContent ?? input?.value?.trim());
 
-       if (!key.length)
+       if (!key || !key.length)
                return;
 
        // Handle remote URL paste
@@ -197,21 +166,20 @@ function addKey(ev, file, fileContent) {
        }
 
        // Prevent duplicates
-       const exists = Array.from(list.querySelectorAll('.item')).some(
-               item => item.getAttribute('data-key') === normalizeKey(key)
-       );
-       if (exists) {
-               ui.addTimeLimitedNotification(_('Add key'), [
-                       E('div', _('The given software repository public key is already present.')),
-               ], 7000, 'notice');
-               return;
-       }
-
-       input.value = '';
-       saveKeyFile(key, file, fileContent)
-               .then(() => listKeyFiles())
-               .then(keys => refreshKeyList(list, keys))
-               .catch(e => ui.addNotification(null, E('p', e.message)));
+       listKeyFiles().then(existingKeys => {
+               if (existingKeys.some(k => normalizeKey(k.key) === normalizeKey(key))) {
+                       ui.addTimeLimitedNotification(_('Add key'), [
+                               E('div', _('The given software repository public key is already present.')),
+                       ], 7000, 'notice');
+                       return;
+               }
+
+               // Save and refresh the UI
+               input.value = '';
+               saveKeyFile(key, file, fileContent)
+                       .then(() => window.location.reload())
+                       .catch(e => ui.addNotification(null, E('p', e.message)));
+       });
 }
 
 function dragKey(ev) {
@@ -224,7 +192,10 @@ function dropKey(ev) {
        ev.preventDefault();
        ev.stopPropagation();
 
-       const input = ev.currentTarget.querySelector('textarea[type="text"]');
+       const input = document.getElementById('key-input');
+
+       if (!input)
+               return;
 
        for (const file of ev.dataTransfer.files) {
                const reader = new FileReader();
@@ -243,47 +214,103 @@ function handleWindowDragDropIgnore(ev) {
 
 return view.extend({
        load() {
-               return determineKeyEnv().then(listKeyFiles);
+               return Promise.all([
+                       determineKeyEnv().then(listKeyFiles),
+               ]);
        },
 
-       render(keys) {
-               const list = E('div', {
-                       class: 'cbi-dynlist',
-                       style: 'max-width: 800px',
-                       dragover: isReadonlyView ? null : dragKey,
-                       drop: isReadonlyView ? null : dropKey
-               }, [
-                       E('div', { class: 'add-item' }, [
-                               E('textarea', {
-                                       id: 'key-input',
-                                       'aria-label': _('Paste or drag repository public key'),
-                                       class: 'cbi-input-text',
-                                       type: 'text',
-                                       style: 'width: 300px; min-height: 120px; ',
-                                       placeholder: _('Paste content of a file, or a URL to a key file, or drag and drop here to upload a software repository public key…'),
-                                       keydown: function(ev) { if (ev.keyCode === 13) addKey(ev); },
-                                       disabled: isReadonlyView
-                               }),
+       render([keys]) {
+
+               const m = new form.JSONMap({
+                       keys: keys,
+                       fup: {},
+               },
+                       _('Repository Public Keys'), _(
+                       _('Each software repository public key (from official or third party repositories) allows packages in lists signed by it to be installed by the package manager.') + '<br/>' +
+                       _('Each key is stored as a file in %s.').format(`<code>${KEYDIR}</code>`)
+               ));
+               m.submit = false;
+               m.reset = false;
+               m.readonly = isReadonlyView;
+
+               let s, o;
+
+               s = m.section(form.TableSection, 'keys');
+               s.anonymous = true;
+               s.nodescriptions = true;
+
+               o = s.option(form.DummyValue, 'filename', _('Name'));
+               o.width = '20%';
+               o = s.option(form.TextValue, 'key', _('Key'));
+               o.readonly = true;
+               o.monospace = true;
+               o.cols = 85;
+               o.rows = 5;
+
+               s.renderRowActions = function (section_id) {
+                       const key = this.map.data.get(this.map.config, section_id);
+                       const isReservedKey = isFileInSafeList(key.filename); 
+
+                       const btns = [
                                E('button', {
-                                       class: 'cbi-button',
-                                       click: ui.createHandlerFn(this, addKey),
-                                       disabled: isReadonlyView
-                               }, _('Add key'))
-                       ])
-               ]);
+                                       'class': 'cbi-button cbi-button-negative remove',
+                                       'click': ui.createHandlerFn(this, this.handleRemove, key),
+                                       'disabled': isReservedKey ? true : null,
+                               }, [_('Delete')]),
+                       ];
 
-               refreshKeyList(list, keys);
-               window.addEventListener('dragover', handleWindowDragDropIgnore);
-               window.addEventListener('drop', handleWindowDragDropIgnore);
-
-               return E('div', {}, [
-                       E('h2', _('Repository Public Keys')),
-                       E('div', { class: 'cbi-section-descr' },
-                               _('Each software repository public key (from official or third party repositories) allows packages in lists signed by it to be installed by the package manager.')),
-                       E('div', { class: 'cbi-section-descr' },
-                               _('Each key is stored as a file in <code>%s</code>.').format(KEYDIR)),
-                       E('div', { class: 'cbi-section-node' }, list)
-               ]);
+                       return E('td', { 'class': 'td middle cbi-section-actions' }, E('div', btns));
+               };
+
+               s.handleRemove = function(key, ev) {
+                       if (isFileInSafeList(key.filename)) {
+                               ui.addTimeLimitedNotification(null, E('p', _('This key is protected and cannot be deleted.')), 3000, 'warning');
+                               return;
+                       }
+
+                       return removeKey(ev, key)
+               };
+
+               s = m.section(form.NamedSection, 'fup');
+
+               o = s.option(form.DummyValue, '_newkey');
+               o.cfgvalue = function(section_id) {
+
+                       const addInput = E('textarea', {
+                               id: 'key-input',
+                               'aria-label': _('Paste or drag repository public key'),
+                               class: 'cbi-input-text',
+                               type: 'text',
+                               style: 'width: 100%; min-height: 120px;',
+                               placeholder: _('Paste content of a file, or a URL to a key file, or drag and drop here to upload a software repository public key…'),
+                               keydown: function(ev) { if (ev.keyCode === 13 && (ev.ctrlKey || ev.metaKey)) addKey(ev); },
+                               disabled: isReadonlyView
+                       });
+
+                       addInput.addEventListener('dragover', handleWindowDragDropIgnore);
+                       addInput.addEventListener('drop', handleWindowDragDropIgnore);
+
+                       const addBtn = E('button', {
+                               class: 'cbi-button',
+                               click: ui.createHandlerFn(this, addKey),
+                               disabled: isReadonlyView
+                       }, _('Add key'));
+
+                       return E('div', {
+                               class: 'cbi-section-node',
+                               dragover: isReadonlyView ? null : dragKey,
+                               drop: isReadonlyView ? null : dropKey
+                       }, [
+                               E('div', { class: 'cbi-section-descr' }, _('Add new repository public key by pasting its content, a file, or a URL.')),
+                               E('div', {
+                                       'style': 'height: 20px',
+                               }, [' ']),
+                               addInput,
+                               E('div', { class: 'right' }, [ addBtn ])
+                       ]);
+               };
+
+               return m.render();
        },
 
        handleSaveApply: null,
git clone https://git.99rst.org/PROJECT