From d6c6423f19d30c81300f6ab1cf7d258fa12a3ced Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 26 Aug 2022 18:05:27 +0530 Subject: [PATCH] admin panel improvements * copy pin list to clipboard * support blob URLs in document metadata query * get metadata history --- www/admin/app-admin.less | 11 ++- www/admin/inner.js | 162 +++++++++++++++++++++++++++++---------- 2 files changed, 133 insertions(+), 40 deletions(-) diff --git a/www/admin/app-admin.less b/www/admin/app-admin.less index e6b7258c5..fd07e76a7 100644 --- a/www/admin/app-admin.less +++ b/www/admin/app-admin.less @@ -327,16 +327,25 @@ .cp-charts-row.heading { font-weight: bold; } + table.cp-metadata-history, table.cp-account-stats, table.cp-block-stats, + table.cp-pin-list, table.cp-document-stats { - @color: #777; + @color: #777; // XXX border: 1px solid @color; margin: 15px; + td, pre { + color: @cryptpad_text_col; + } td { max-width: 60vw; // XXX border: 1px solid @color; padding: 5px; + .scroll { + max-width: 500px; + overflow-x: auto; + } } } } diff --git a/www/admin/inner.js b/www/admin/inner.js index 258b31760..e0f988c6f 100644 --- a/www/admin/inner.js +++ b/www/admin/inner.js @@ -346,7 +346,6 @@ define([ return button; }; - Messages.admin_reportContent = "Report content"; // XXX var reportContentLabel = () => { return h('span', [ Messages.admin_reportContent, @@ -356,6 +355,16 @@ define([ ]); }; + var DOCUMENT_TYPES = { + 32: 'channel', + 48: 'file', + 33: 'ephemeral', + 34: 'broadcast', + }; + var inferDocumentType = id => { + return DOCUMENT_TYPES[typeof(id) === 'string' && id.length] || 'unknown'; + }; + var renderAccountData = function (data) { var tableObj = makeMetadataTable('cp-account-stats'); var row = tableObj.row; @@ -423,10 +432,35 @@ define([ console.error(err); return void UI.warn(Messages.error); } + + var table = makeMetadataTable('cp-pin-list').table; + var row = id => { + var type = inferDocumentType(id); + table.appendChild(h('tr', [ + h('td', h('code', id)), + h('td', type), + ])); + }; + var P = pins.slice().sort((a, b) => a.length - b.length); + P.map(row); + + UI.confirm(table, yes => { + if (!yes) { return; } + var content = P.join('\n'); + if (Clipboard.copy.multiline(content)) { + UI.log(Messages.genericCopySuccess); + } else { + UI.warn(Messages.error); + } + }, { + wide: true, + ok: Messages.copyToClipboard, + }); + // XXX format as two-column table (id, type) // XXX add ['copy-to-clipboard', 'close'] buttons - UI.alert(h('ul', P.map(p => h('li', h('code', p))))); + //UI.alert(h('ul', P.map(p => h('li', h('code', p))))); }); }; @@ -446,7 +480,8 @@ define([ var pinHistoryButton = primary(Messages.ui_fetch, getHistoryHandler); disable($(pinHistoryButton)); - row(Messages.admin_getFullPinHistory, pinHistoryButton); // XXX display modal with pre and buttons to copy/download + // XXX pin history is not implemented + //row(Messages.admin_getFullPinHistory, pinHistoryButton); // XXX display modal with pre and buttons to copy/download // archive pin log var archiveHandler = () => { @@ -470,6 +505,7 @@ define([ // archive owned documents // XXX accessibility, tooltips +/* // XXX not implemented var archiveDocuments = () => { justifyRestorationDialog(Messages.admin_archiveDocumentsConfirm, reason => { sframeCommand('ARCHIVE_OWNED_DOCUMENTS', { @@ -485,6 +521,7 @@ define([ var archiveDocumentsButton = danger(Messages.admin_archiveButton, archiveDocuments); disable($(archiveDocumentsButton)); // XXX row(Messages.admin_archiveOwnedAccountDocuments, archiveDocumentsButton); +*/ } row(reportContentLabel, copyToClipboard(data)); @@ -580,13 +617,7 @@ define([ generated: +new Date(), id: id, }; - var types = { - 32: 'channel', - 48: 'file', - 33: 'ephemeral', - 34: 'broadcast', - }; - data.type = types[typeof(id) === 'string' && id.length] || 'unknown'; + data.type = inferDocumentType(id); nThen(function (w) { // XXX if (data.type !== 'channel') { return; } @@ -599,11 +630,9 @@ define([ })); }).nThen(function (w) { if (data.type !== 'file') { return; } - w = w; // XXX get file data? - // XXX atime, mtime, ctime ? + w = w; // XXX get blob-specific data? (atime, mtime, ctime) }).nThen(function (w) { sframeCommand("GET_DOCUMENT_SIZE", id, w(function (err, res) { - console.info('got document size', err, res); if (err) { return void console.error(err); } if (!(Array.isArray(res) && typeof(res[0]) === 'number')) { return void console.error("NO_SIZE"); @@ -621,7 +650,7 @@ define([ // whether currently open if (data.type !== 'channel') { return; } sframeCommand('GET_CACHED_CHANNEL_METADATA', id, w(function (err, res) { - console.info("cached channel metadata", err, res); + //console.info("cached channel metadata", err, res); if (err === 'ENOENT') { data.currentlyOpen = false; return; @@ -631,9 +660,6 @@ define([ if (!Array.isArray(res) || !res[0]) { return void console.error(res); } data.currentlyOpen = true; })); - }).nThen(function (/* w */) { - // offset time if exists - }).nThen(function (w) { // status (live, archived, unknown) if (!['channel', 'file'].includes(data.type)) { return; } @@ -645,7 +671,7 @@ define([ } data.live = res[0].live; data.archived = res[0].archived; - console.error("get channel status", err, res); + //console.error("get channel status", err, res); })); }).nThen(function () { // for easy readability when copying to clipboard @@ -661,15 +687,11 @@ define([ }); }; - Messages.admin_getFullPinHistory = 'Fetch full pin history (not implemented)'; // XXX - + Messages.admin_getFullPinHistory = 'Pin history'; // XXX Messages.admin_archivePinLogConfirm = "All content in this user's drive will be un-listed, meaning it may be deleted if it is not in any other drive."; // XXX - Messages.admin_archiveOwnedAccountDocuments = "Archive this account's owned documents (not implemented)"; // XXX Messages.admin_archiveOwnedDocumentsConfirm = "All content owned exclusively by this user will be archived. This means their documents, drive, and accounts will be made inaccessible. This action cannot be undone. Please save the full pin list before proceeding to ensure individual documents can be restored."; // XXX - Messages.admin_getRawMetadata = 'Fetch full metadata history (not implemented)'; // XXX - var localizeType = function (type) { var o = { channel: Messages.type.doc, @@ -695,20 +717,66 @@ define([ console.error(err2); } - row(Messages.admin_documentCreationTime, maybeDate(data.created)); - row(Messages.admin_documentModifiedTime, maybeDate(data.lastModified)); - row(Messages.admin_currentlyOpen, localizeState(data.currentlyOpen)); - row(Messages.admin_channelAvailable, localizeState(data.live)); - row(Messages.admin_channelArchived, localizeState(data.archived)); - // actions // get raw metadata history var metadataHistoryButton = primary(Messages.ui_fetch, function () { - UI.warn('NOT_IMPLEMENTED'); // XXX + sframeCommand('GET_METADATA_HISTORY', data.id, (err, result) => { + if (err) { + UI.warn(Messages.error); + return void console.error(err); + } + if (!Array.isArray(result)) { + UI.warn(Messages.error); + return void console.error("Expected an array"); + } + var tableObj = makeMetadataTable('cp-metadata-history'); + var row = items => { + tableObj.table.appendChild(h('tr', items.map(item => { + return h('td', item); + }))); + }; + var scroll = el => h('div.scroll', el); + result.forEach(item => { + var raw = JSON.stringify(item); + var time; + var last; + if (Array.isArray(item)) { + last = item[item.length - 1]; + if (typeof(last) === 'number') { time = last; } + } else if (item && typeof(item) === 'object') { + time = item.created; + } + row([ + h('small', maybeDate(time)), // time + scroll(h('code', raw)), // Raw + ]); + }); + + UI.confirm(tableObj.table, (yes) => { + if (!yes) { return; } + var content = result.map(line => JSON.stringify(line)).join('\n'); + if (Clipboard.copy.multiline(content)) { + UI.log(Messages.genericCopySuccess); + } else { + UI.warn(Messages.error); + } + }, { + wide: true, + ok: Messages.copyToClipboard, + }); + }); }); - disable($(metadataHistoryButton)); row(Messages.admin_getRawMetadata, metadataHistoryButton); + + row(Messages.admin_documentCreationTime, maybeDate(data.created)); + row(Messages.admin_documentModifiedTime, maybeDate(data.lastModified)); + row(Messages.admin_currentlyOpen, localizeState(data.currentlyOpen)); } + if (['file', 'channel'].includes(data.type)) { + row(Messages.admin_channelAvailable, localizeState(data.live)); + row(Messages.admin_channelArchived, localizeState(data.archived)); + } + if (data.type === 'file') { // XXX what to do for files? @@ -727,7 +795,7 @@ define([ console.error(err); return void UI.warn(Messages.error); } - UI.log(Messages.archivedFromServer); // XXX success ? + UI.log(Messages.ui_success); //archivedFromServer); // XXX success ? disable($(archiveDocumentButton)); }); }); @@ -761,7 +829,7 @@ define([ return tableObj.table; }; - create['document-metadata'] = function () { // XXX + create['document-metadata'] = function () { var key = 'document-metadata'; var $div = makeBlock(key, true); @@ -781,6 +849,20 @@ define([ var $password = $(passwordContainer).find('input'); $password.attr('placeholder', Messages.admin_archiveInput2); + var getBlobId = pathname => { + var parts; + try { + if (typeof(pathname) !== 'string') { return; } + parts = pathname.split('/').filter(Boolean); + if (parts.length !== 3) { return; } + if (parts[0] !== 'blob') { return; } + if (parts[1].length !== 2) { return; } + if (parts[2].length !== 48) { return; } + if (!parts[2].startsWith(parts[1])) { return; } + } catch (err) { return false; } + return parts[2]; + }; + var pending = false; var getInputState = function () { var val = $input.val().trim(); @@ -806,15 +888,17 @@ define([ } catch (err) {} if (!url) { return state; } // invalid - var parsed = Hash.isValidHref(val); - if (!parsed || !parsed.hashData) { - // XXX invalid input causes the password field to be displayed, ie: - // "drive": "eafc6ed83efa917a2d732b96e49c322b", - state.passwordRequired = true; - state.valid = false; + + // recognize URLs of the form: /blob/f1/f1338921fe8a73ed5401780d2147f725deeb9e3329f0f01e + var blobId = getBlobId(url.pathname); + if (blobId) { + state.valid = true; + state.id = blobId; return state; } + var parsed = Hash.isValidHref(val); + if (!parsed || !parsed.hashData) { return state; } if (parsed.hashData.version === 3) { state.id = parsed.hashData.channel; state.valid = true;