ui: content: show namespaces also inline and rework node type detection

this not only makes the action disable/hide checks simpler, but also
prepares the view a bit for the idea of adding a new API endpoint
that returns the whole datastore content tree as structured JSON so
that it can be directly loaded into a tree store.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
This commit is contained in:
Thomas Lamprecht 2022-05-12 09:17:54 +02:00
parent fe79687c59
commit 7e8b24bd8c
1 changed files with 110 additions and 30 deletions

View File

@ -14,6 +14,8 @@ Ext.define('pbs-data-store-snapshots', {
'verification', 'verification',
'fingerprint', 'fingerprint',
{ name: 'size', type: 'int', allowNull: true }, { name: 'size', type: 'int', allowNull: true },
{ name: 'sortWeight', type: 'int', allowNull: true },
{ name: 'ty', type: 'string', allowNull: true },
{ {
name: 'crypt-mode', name: 'crypt-mode',
type: 'boolean', type: 'boolean',
@ -50,7 +52,7 @@ Ext.define('PBS.DataStoreContent', {
alias: 'widget.pbsDataStoreContent', alias: 'widget.pbsDataStoreContent',
mixins: ['Proxmox.Mixin.CBind'], mixins: ['Proxmox.Mixin.CBind'],
rootVisible: true, rootVisible: false,
title: gettext('Content'), title: gettext('Content'),
@ -69,7 +71,7 @@ Ext.define('PBS.DataStoreContent', {
this.store.on('load', this.onLoad, this); this.store.on('load', this.onLoad, this);
view.getStore().setSorters([ view.getStore().setSorters([
'backup-group', 'sortWeight',
'text', 'text',
'backup-time', 'backup-time',
]); ]);
@ -77,13 +79,28 @@ Ext.define('PBS.DataStoreContent', {
}, },
control: { control: {
'#': { // view
rowdblclick: 'rowDoubleClicked',
},
'pbsNamespaceSelector': { 'pbsNamespaceSelector': {
change: 'nsChange', change: 'nsChange',
}, },
}, },
rowDoubleClicked: function(table, rec, el, rowId, ev) {
if (rec?.data?.ty !== 'ns') {
return;
}
this.nsChange(null, rec.data.ns);
},
nsChange: function(field, value) { nsChange: function(field, value) {
let view = this.getView(); let view = this.getView();
if (field === null) {
field = view.down('pbsNamespaceSelector');
field.setValue(value);
return;
}
view.namespace = value; view.namespace = value;
this.reload(); this.reload();
}, },
@ -162,7 +179,22 @@ Ext.define('PBS.DataStoreContent', {
} }
}, },
onLoad: function(store, records, success, operation) { loadNamespaceFromSameLevel: async function() {
let view = this.getView();
try {
let url = `/api2/extjs/admin/datastore/${view.datastore}/namespace?max-depth=1`;
if (view.namespace && view.namespace !== '') {
url += `&parent=${encodeURIComponent(view.namespace)}`;
}
let { result: { data: ns } } = await Proxmox.Async.api2({ url });
return ns;
} catch (err) {
console.debug(err);
}
return [];
},
onLoad: async function(store, records, success, operation) {
let me = this; let me = this;
let view = this.getView(); let view = this.getView();
@ -171,6 +203,8 @@ Ext.define('PBS.DataStoreContent', {
return; return;
} }
let namespaces = await me.loadNamespaceFromSameLevel();
let groups = this.getRecordGroups(records); let groups = this.getRecordGroups(records);
let selected; let selected;
@ -207,6 +241,7 @@ Ext.define('PBS.DataStoreContent', {
data.leaf = false; data.leaf = false;
data.cls = 'no-leaf-icons'; data.cls = 'no-leaf-icons';
data.matchesFilter = true; data.matchesFilter = true;
data.ty = 'dir';
data.expanded = !!expanded[data.text]; data.expanded = !!expanded[data.text];
@ -217,6 +252,7 @@ Ext.define('PBS.DataStoreContent', {
file.fingerprint = data.fingerprint; file.fingerprint = data.fingerprint;
file.leaf = true; file.leaf = true;
file.matchesFilter = true; file.matchesFilter = true;
file.ty = 'file';
data.children.push(file); data.children.push(file);
} }
@ -271,22 +307,62 @@ Ext.define('PBS.DataStoreContent', {
crypt.count = group.count; crypt.count = group.count;
group['crypt-mode'] = PBS.Utils.calculateCryptMode(crypt); group['crypt-mode'] = PBS.Utils.calculateCryptMode(crypt);
group.expanded = !!expanded[name]; group.expanded = !!expanded[name];
group.sortWeight = 0;
group.ty = 'group';
children.push(group); children.push(group);
} }
for (const item of namespaces) {
if (item.ns === view.namespace || (!view.namespace && item.ns === '')) {
continue;
}
children.push({
text: item.ns,
iconCls: 'fa fa-object-group',
expanded: true,
expandable: false,
ns: (view.namespaces ?? '') !== '' ? `/${item.ns}` : item.ns,
ty: 'ns',
sortWeight: 10,
//qtip: gettext('Double-click to browse namespace.'),
leaf: true,
});
}
let isRootNS = !view.namespace || view.namespace === ''; let isRootNS = !view.namespace || view.namespace === '';
let rootText = isRootNS let rootText = isRootNS
? gettext('Root Namespace') ? gettext('Root Namespace')
: Ext.String.format(gettext("Namespace '{0}'"), view.namespace); : Ext.String.format(gettext("Namespace '{0}'"), view.namespace);
view.setRootNode({ let topNodes = [];
if (!isRootNS) {
let parentNS = view.namespace.split('/').slice(0, -1).join('/');
topNodes.push({
text: `.. (${parentNS === '' ? gettext('Root') : parentNS})`,
iconCls: 'fa fa-level-up',
//qtip: gettext('Double-click to go one namespace level up.'),
ty: 'ns',
ns: parentNS,
sortWeight: -10,
leaf: true,
});
}
topNodes.push({
text: rootText, text: rootText,
iconCls: "fa fa-" + (isRootNS ? 'database' : 'object-group'), iconCls: "fa fa-" + (isRootNS ? 'database' : 'object-group'),
expanded: true, expanded: true,
expandable: false, expandable: false,
sortWeight: -5,
root: true, // fake root
ty: 'ns',
children: children, children: children,
}); });
view.setRootNode({
expanded: true,
children: topNodes,
});
if (!children.length) { if (!children.length) {
view.setEmptyText(Ext.String.format( view.setEmptyText(Ext.String.format(
gettext('No accessible snapshots found in namespace {0}'), gettext('No accessible snapshots found in namespace {0}'),
@ -668,6 +744,11 @@ Ext.define('PBS.DataStoreContent', {
let me = this; let me = this;
let view = me.getView(); let view = me.getView();
if (rec.data.ty === 'ns') {
me.nsChange(null, rec.data.ns);
return;
}
if (!(rec && rec.data)) return; if (!(rec && rec.data)) return;
let data = rec.parentNode.data; let data = rec.parentNode.data;
@ -711,7 +792,8 @@ Ext.define('PBS.DataStoreContent', {
let store = view.getStore(); let store = view.getStore();
if (!value && value !== 0) { if (!value && value !== 0) {
store.clearFilter(); store.clearFilter();
store.getRoot().collapseChildren(true); // only collapse the children below our toplevel namespace "root"
store.getRoot().lastChild.collapseChildren(true);
tf.triggers.clear.setVisible(false); tf.triggers.clear.setVisible(false);
return; return;
} }
@ -844,64 +926,62 @@ Ext.define('PBS.DataStoreContent', {
{ {
handler: 'onVerify', handler: 'onVerify',
getTip: (v, m, rec) => Ext.String.format(gettext("Verify '{0}'"), v), getTip: (v, m, rec) => Ext.String.format(gettext("Verify '{0}'"), v),
getClass: (v, m, rec) => rec.data.root || rec.data.leaf ? 'pmx-hidden' : 'pve-icon-verify-lettering', getClass: (v, m, { data }) => data.ty === 'group' || data.ty === 'dir' ? 'pve-icon-verify-lettering' : 'pmx-hidden',
isActionDisabled: (v, r, c, i, rec) => !!rec.data.leaf, isActionDisabled: (v, r, c, i, rec) => !!rec.data.leaf,
}, },
{ {
handler: 'onChangeOwner', handler: 'onChangeOwner',
getClass: (v, m, rec) => rec.parentNode && rec.parentNode.id ==='root' ? 'fa fa-user' : 'pmx-hidden', getClass: (v, m, { data }) => data.ty === 'group' ? 'fa fa-user' : 'pmx-hidden',
getTip: (v, m, rec) => Ext.String.format(gettext("Change owner of '{0}'"), v), getTip: (v, m, rec) => Ext.String.format(gettext("Change owner of '{0}'"), v),
isActionDisabled: (v, r, c, i, rec) => !rec.parentNode || rec.parentNode.id !=='root', isActionDisabled: (v, r, c, i, { data }) => data.ty !== 'group',
}, },
{ {
handler: 'onPrune', handler: 'onPrune',
getTip: (v, m, rec) => Ext.String.format(gettext("Prune '{0}'"), v), getTip: (v, m, rec) => Ext.String.format(gettext("Prune '{0}'"), v),
getClass: (v, m, rec) => rec.parentNode && rec.parentNode.id ==='root' ? 'fa fa-scissors' : 'pmx-hidden', getClass: (v, m, { data }) => data.ty === 'group' ? 'fa fa-scissors' : 'pmx-hidden',
isActionDisabled: (v, r, c, i, rec) => rec.parentNode?.id !=='root', isActionDisabled: (v, r, c, i, { data }) => data.ty !== 'group',
}, },
{ {
handler: 'onProtectionChange', handler: 'onProtectionChange',
getTip: (v, m, rec) => Ext.String.format(gettext("Change protection of '{0}'"), v), getTip: (v, m, rec) => Ext.String.format(gettext("Change protection of '{0}'"), v),
getClass: (v, m, rec) => { getClass: (v, m, rec) => {
if (!rec.data.leaf && rec.parentNode && rec.parentNode.id !== 'root') { if (rec.data.ty === 'dir') {
let extraCls = rec.data.protected ? 'good' : 'faded'; let extraCls = rec.data.protected ? 'good' : 'faded';
return `fa fa-shield ${extraCls}`; return `fa fa-shield ${extraCls}`;
} }
return 'pmx-hidden'; return 'pmx-hidden';
}, },
isActionDisabled: (v, r, c, i, rec) => !!rec.data.leaf || !rec.parentNode || rec.parentNode.id === 'root', isActionDisabled: (v, r, c, i, rec) => rec.data.ty !== 'dir',
}, },
{ {
handler: 'onForget', handler: 'onForget',
getTip: (v, m, rec) => rec.parentNode?.id !=='root' getTip: (v, m, { data }) => data ==='dir'
? Ext.String.format(gettext("Permanently forget snapshot '{0}'"), v) ? Ext.String.format(gettext("Permanently forget snapshot '{0}'"), v)
: Ext.String.format(gettext("Permanently forget group '{0}'"), v), : Ext.String.format(gettext("Permanently forget group '{0}'"), v),
getClass: (v, m, rec) => !(rec.data.leaf || rec.data.root) ? 'fa critical fa-trash-o' : 'pmx-hidden', getClass: (v, m, { data }) =>
isActionDisabled: (v, r, c, i, rec) => !!rec.data.leaf, data.ty === 'group' || data.ty === 'dir' ? 'fa critical fa-trash-o' : 'pmx-hidden',
isActionDisabled: (v, r, c, i, { data }) => !(data.ty === 'group' || data.ty === 'dir'),
}, },
{ {
handler: 'downloadFile', handler: 'downloadFile',
getTip: (v, m, rec) => Ext.String.format(gettext("Download '{0}'"), v), getTip: (v, m, rec) => Ext.String.format(gettext("Download '{0}'"), v),
getClass: (v, m, rec) => rec.data.leaf && rec.data.filename ? 'fa fa-download' : 'pmx-hidden', getClass: (v, m, { data }) => data.ty === 'file' ? 'fa fa-download' : 'pmx-hidden',
isActionDisabled: (v, r, c, i, rec) => !rec.data.leaf || !rec.data.filename || rec.data['crypt-mode'] > 2, isActionDisabled: (v, r, c, i, rec) => rec.data.ty !== 'file' || rec.data['crypt-mode'] > 2,
}, },
{ {
handler: 'openPxarBrowser', handler: 'openPxarBrowser',
tooltip: gettext('Browse'), tooltip: gettext('Browse'),
getClass: (v, m, rec) => { getClass: (v, m, { data }) => {
let data = rec.data; if (
if (data.leaf && data.filename && data.filename.endsWith('pxar.didx')) { (data.ty === 'file' && data.filename.endsWith('pxar.didx')) ||
(data.ty === 'ns' && !data.root)
) {
return 'fa fa-folder-open-o'; return 'fa fa-folder-open-o';
} }
return 'pmx-hidden'; return 'pmx-hidden';
}, },
isActionDisabled: (v, r, c, i, rec) => { isActionDisabled: (v, r, c, i, { data }) =>
let data = rec.data; !(data.ty === 'file' && data.filename.endsWith('pxar.didx') && data['crypt-mode'] < 3) && data.ty !== 'ns',
return !(data.leaf &&
data.filename &&
data.filename.endsWith('pxar.didx') &&
data['crypt-mode'] < 3);
},
}, },
], ],
}, },
@ -917,8 +997,8 @@ Ext.define('PBS.DataStoreContent', {
header: gettext("Size"), header: gettext("Size"),
sortable: true, sortable: true,
dataIndex: 'size', dataIndex: 'size',
renderer: (v, meta, record) => { renderer: (v, meta, { data }) => {
if ((record.data.text === 'client.log.blob' && v === undefined) || record.data.root) { if ((data.text === 'client.log.blob' && v === undefined) || (data.ty !== 'dir' && data.ty !== 'file')) {
return ''; return '';
} }
if (v === undefined || v === null) { if (v === undefined || v === null) {
@ -992,7 +1072,7 @@ Ext.define('PBS.DataStoreContent', {
} }
}, },
renderer: (v, meta, record) => { renderer: (v, meta, record) => {
if (!record.parentNode) { if (record.data.ty === 'ns') {
return ''; // TODO: accumulate verify of all groups into root NS node? return ''; // TODO: accumulate verify of all groups into root NS node?
} }
let i = (cls, txt) => `<i class="fa fa-fw fa-${cls}"></i> ${txt}`; let i = (cls, txt) => `<i class="fa fa-fw fa-${cls}"></i> ${txt}`;