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',
'fingerprint',
{ name: 'size', type: 'int', allowNull: true },
{ name: 'sortWeight', type: 'int', allowNull: true },
{ name: 'ty', type: 'string', allowNull: true },
{
name: 'crypt-mode',
type: 'boolean',
@ -50,7 +52,7 @@ Ext.define('PBS.DataStoreContent', {
alias: 'widget.pbsDataStoreContent',
mixins: ['Proxmox.Mixin.CBind'],
rootVisible: true,
rootVisible: false,
title: gettext('Content'),
@ -69,7 +71,7 @@ Ext.define('PBS.DataStoreContent', {
this.store.on('load', this.onLoad, this);
view.getStore().setSorters([
'backup-group',
'sortWeight',
'text',
'backup-time',
]);
@ -77,13 +79,28 @@ Ext.define('PBS.DataStoreContent', {
},
control: {
'#': { // view
rowdblclick: 'rowDoubleClicked',
},
'pbsNamespaceSelector': {
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) {
let view = this.getView();
if (field === null) {
field = view.down('pbsNamespaceSelector');
field.setValue(value);
return;
}
view.namespace = value;
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 view = this.getView();
@ -171,6 +203,8 @@ Ext.define('PBS.DataStoreContent', {
return;
}
let namespaces = await me.loadNamespaceFromSameLevel();
let groups = this.getRecordGroups(records);
let selected;
@ -207,6 +241,7 @@ Ext.define('PBS.DataStoreContent', {
data.leaf = false;
data.cls = 'no-leaf-icons';
data.matchesFilter = true;
data.ty = 'dir';
data.expanded = !!expanded[data.text];
@ -217,6 +252,7 @@ Ext.define('PBS.DataStoreContent', {
file.fingerprint = data.fingerprint;
file.leaf = true;
file.matchesFilter = true;
file.ty = 'file';
data.children.push(file);
}
@ -271,22 +307,62 @@ Ext.define('PBS.DataStoreContent', {
crypt.count = group.count;
group['crypt-mode'] = PBS.Utils.calculateCryptMode(crypt);
group.expanded = !!expanded[name];
group.sortWeight = 0;
group.ty = '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 rootText = isRootNS
? gettext('Root 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,
iconCls: "fa fa-" + (isRootNS ? 'database' : 'object-group'),
expanded: true,
expandable: false,
sortWeight: -5,
root: true, // fake root
ty: 'ns',
children: children,
});
view.setRootNode({
expanded: true,
children: topNodes,
});
if (!children.length) {
view.setEmptyText(Ext.String.format(
gettext('No accessible snapshots found in namespace {0}'),
@ -668,6 +744,11 @@ Ext.define('PBS.DataStoreContent', {
let me = this;
let view = me.getView();
if (rec.data.ty === 'ns') {
me.nsChange(null, rec.data.ns);
return;
}
if (!(rec && rec.data)) return;
let data = rec.parentNode.data;
@ -711,7 +792,8 @@ Ext.define('PBS.DataStoreContent', {
let store = view.getStore();
if (!value && value !== 0) {
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);
return;
}
@ -844,64 +926,62 @@ Ext.define('PBS.DataStoreContent', {
{
handler: 'onVerify',
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,
},
{
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),
isActionDisabled: (v, r, c, i, rec) => !rec.parentNode || rec.parentNode.id !=='root',
isActionDisabled: (v, r, c, i, { data }) => data.ty !== 'group',
},
{
handler: 'onPrune',
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',
isActionDisabled: (v, r, c, i, rec) => rec.parentNode?.id !=='root',
getClass: (v, m, { data }) => data.ty === 'group' ? 'fa fa-scissors' : 'pmx-hidden',
isActionDisabled: (v, r, c, i, { data }) => data.ty !== 'group',
},
{
handler: 'onProtectionChange',
getTip: (v, m, rec) => Ext.String.format(gettext("Change protection of '{0}'"), v),
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';
return `fa fa-shield ${extraCls}`;
}
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',
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 group '{0}'"), v),
getClass: (v, m, rec) => !(rec.data.leaf || rec.data.root) ? 'fa critical fa-trash-o' : 'pmx-hidden',
isActionDisabled: (v, r, c, i, rec) => !!rec.data.leaf,
getClass: (v, m, { data }) =>
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',
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',
isActionDisabled: (v, r, c, i, rec) => !rec.data.leaf || !rec.data.filename || rec.data['crypt-mode'] > 2,
getClass: (v, m, { data }) => data.ty === 'file' ? 'fa fa-download' : 'pmx-hidden',
isActionDisabled: (v, r, c, i, rec) => rec.data.ty !== 'file' || rec.data['crypt-mode'] > 2,
},
{
handler: 'openPxarBrowser',
tooltip: gettext('Browse'),
getClass: (v, m, rec) => {
let data = rec.data;
if (data.leaf && data.filename && data.filename.endsWith('pxar.didx')) {
getClass: (v, m, { data }) => {
if (
(data.ty === 'file' && data.filename.endsWith('pxar.didx')) ||
(data.ty === 'ns' && !data.root)
) {
return 'fa fa-folder-open-o';
}
return 'pmx-hidden';
},
isActionDisabled: (v, r, c, i, rec) => {
let data = rec.data;
return !(data.leaf &&
data.filename &&
data.filename.endsWith('pxar.didx') &&
data['crypt-mode'] < 3);
},
isActionDisabled: (v, r, c, i, { data }) =>
!(data.ty === 'file' && data.filename.endsWith('pxar.didx') && data['crypt-mode'] < 3) && data.ty !== 'ns',
},
],
},
@ -917,8 +997,8 @@ Ext.define('PBS.DataStoreContent', {
header: gettext("Size"),
sortable: true,
dataIndex: 'size',
renderer: (v, meta, record) => {
if ((record.data.text === 'client.log.blob' && v === undefined) || record.data.root) {
renderer: (v, meta, { data }) => {
if ((data.text === 'client.log.blob' && v === undefined) || (data.ty !== 'dir' && data.ty !== 'file')) {
return '';
}
if (v === undefined || v === null) {
@ -992,7 +1072,7 @@ Ext.define('PBS.DataStoreContent', {
}
},
renderer: (v, meta, record) => {
if (!record.parentNode) {
if (record.data.ty === 'ns') {
return ''; // TODO: accumulate verify of all groups into root NS node?
}
let i = (cls, txt) => `<i class="fa fa-fw fa-${cls}"></i> ${txt}`;