don't shout: s/APIVIEWER/APIViewer/
authorThomas Lamprecht <t.lamprecht@proxmox.com>
Wed, 2 Jun 2021 12:43:13 +0000 (14:43 +0200)
committerThomas Lamprecht <t.lamprecht@proxmox.com>
Wed, 2 Jun 2021 12:43:13 +0000 (14:43 +0200)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
debian/proxmox-widget-toolkit-dev.install
src/api-viewer/APIVIEWER.js [deleted file]
src/api-viewer/APIViewer.js [new file with mode: 0644]

index 514d1d0..620d69f 100644 (file)
@@ -1,2 +1,2 @@
 Toolkit.js /usr/share/javascript/proxmox-widget-toolkit-dev/
-api-viewer/APIVIEWER.js /usr/share/javascript/proxmox-widget-toolkit-dev/
+api-viewer/APIViewer.js /usr/share/javascript/proxmox-widget-toolkit-dev/
diff --git a/src/api-viewer/APIVIEWER.js b/src/api-viewer/APIVIEWER.js
deleted file mode 100644 (file)
index c2864ed..0000000
+++ /dev/null
@@ -1,529 +0,0 @@
-// avoid errors when running without development tools
-if (!Ext.isDefined(Ext.global.console)) {
-    var console = {
-        dir: function() {},
-        log: function() {}
-    };
-}
-
-Ext.onReady(function() {
-
-    Ext.define('pmx-param-schema', {
-        extend: 'Ext.data.Model',
-        fields:  [
-           'name', 'type', 'typetext', 'description', 'verbose_description',
-           'enum', 'minimum', 'maximum', 'minLength', 'maxLength',
-           'pattern', 'title', 'requires', 'format', 'default',
-           'disallow', 'extends', 'links',
-           {
-               name: 'optional',
-               type: 'boolean'
-           }
-       ]
-    });
-
-    var store = Ext.define('pmx-updated-treestore', {
-       extend: 'Ext.data.TreeStore',
-       model: Ext.define('pmx-api-doc', {
-            extend: 'Ext.data.Model',
-            fields:  [
-               'path', 'info', 'text',
-           ]
-       }),
-        proxy: {
-            type: 'memory',
-            data: pmxapi
-        },
-        sorters: [{
-            property: 'leaf',
-            direction: 'ASC'
-        }, {
-            property: 'text',
-            direction: 'ASC'
-       }],
-       filterer: 'bottomup',
-       doFilter: function(node) {
-           this.filterNodes(node, this.getFilters().getFilterFn(), true);
-       },
-
-       filterNodes: function(node, filterFn, parentVisible) {
-           var me = this,
-               bottomUpFiltering = me.filterer === 'bottomup',
-               match = filterFn(node) && parentVisible || (node.isRoot() && !me.getRootVisible()),
-               childNodes = node.childNodes,
-               len = childNodes && childNodes.length, i, matchingChildren;
-
-           if (len) {
-               for (i = 0; i < len; ++i) {
-                   matchingChildren = me.filterNodes(childNodes[i], filterFn, match || bottomUpFiltering) || matchingChildren;
-               }
-               if (bottomUpFiltering) {
-                   match = matchingChildren || match;
-               }
-           }
-
-           node.set("visible", match, me._silentOptions);
-           return match;
-       },
-
-    }).create();
-
-    var render_description = function(value, metaData, record) {
-       var pdef = record.data;
-
-       value = pdef.verbose_description || value;
-
-       // TODO: try to render asciidoc correctly
-
-       metaData.style = 'white-space:pre-wrap;'
-
-       return Ext.htmlEncode(value);
-    };
-
-    var render_type = function(value, metaData, record) {
-       var pdef = record.data;
-
-       return pdef['enum'] ? 'enum' : (pdef.type || 'string');
-    };
-
-    let render_simple_format = function(pdef, type_fallback) {
-       if (pdef.typetext)
-           return pdef.typetext;
-
-       if (pdef['enum'])
-           return pdef['enum'].join(' | ');
-
-       if (pdef.format)
-           return pdef.format;
-
-       if (pdef.pattern)
-           return pdef.pattern;
-
-       if (pdef.type === 'boolean')
-           return `<true|false>`;
-
-       if (type_fallback && pdef.type)
-           return `<${pdef.type}>`;
-
-       return;
-    };
-
-    let render_format = function(value, metaData, record) {
-       let pdef = record.data;
-
-       metaData.style = 'white-space:normal;'
-
-       if (pdef.type === 'array' && pdef.items) {
-           let format = render_simple_format(pdef.items, true);
-           return `[${Ext.htmlEncode(format)}, ...]`;
-       }
-
-       return Ext.htmlEncode(render_simple_format(pdef) || '');
-    };
-
-    var real_path = function(path) {
-       return path.replace(/^.*\/_upgrade_(\/)?/, "/");
-    };
-
-    var permission_text = function(permission) {
-       let permhtml = "";
-
-       if (permission.user) {
-           if (!permission.description) {
-               if (permission.user === 'world') {
-                   permhtml += "Accessible without any authentication.";
-               } else if (permission.user === 'all') {
-                   permhtml += "Accessible by all authenticated users.";
-               } else {
-                   permhtml += 'Onyl accessible by user "' +
-                       permission.user + '"';
-               }
-           }
-       } else if (permission.check) {
-           permhtml += "<pre>Check: " +
-               Ext.htmlEncode(Ext.JSON.encode(permission.check))  + "</pre>";
-       } else if (permission.userParam) {
-           permhtml += `<div>Check if user matches parameter '${permission.userParam}'`;
-       } else if (permission.or) {
-           permhtml += "<div>Or<div style='padding-left: 10px;'>";
-           Ext.Array.each(permission.or, function(sub_permission) {
-               permhtml += permission_text(sub_permission);
-           })
-           permhtml += "</div></div>";
-       } else if (permission.and) {
-           permhtml += "<div>And<div style='padding-left: 10px;'>";
-           Ext.Array.each(permission.and, function(sub_permission) {
-               permhtml += permission_text(sub_permission);
-           })
-           permhtml += "</div></div>";
-       } else {
-           //console.log(permission);
-           permhtml += "Unknown syntax!";
-       }
-
-       return permhtml;
-    };
-
-    var render_docu = function(data) {
-       var md = data.info;
-
-       // console.dir(data);
-
-       var items = [];
-
-       var clicmdhash = {
-           GET: 'get',
-           POST: 'create',
-           PUT: 'set',
-           DELETE: 'delete'
-       };
-
-       Ext.Array.each(['GET', 'POST', 'PUT', 'DELETE'], function(method) {
-           var info = md[method];
-           if (info) {
-
-               var usage = "";
-
-               usage += "<table><tr><td>HTTP:&nbsp;&nbsp;&nbsp;</td><td>"
-                   + method + " " + real_path("/api2/json" + data.path) + "</td></tr>";
-
-               if (typeof cliusage === 'function') {
-                   usage += cliusage(method, real_path(data.path));
-               }
-
-               var sections = [
-                   {
-                       title: 'Description',
-                       html: Ext.htmlEncode(info.description),
-                       bodyPadding: 10
-                   },
-                   {
-                       title: 'Usage',
-                       html: usage,
-                       bodyPadding: 10
-                   }
-               ];
-
-               if (info.parameters && info.parameters.properties) {
-
-                   var pstore = Ext.create('Ext.data.Store', {
-                       model: 'pmx-param-schema',
-                       proxy: {
-                           type: 'memory'
-                       },
-                       groupField: 'optional',
-                       sorters: [
-                           {
-                               property: 'name',
-                               direction: 'ASC'
-                           }
-                       ]
-                   });
-
-                   Ext.Object.each(info.parameters.properties, function(name, pdef) {
-                       pdef.name = name;
-                       pstore.add(pdef);
-                   });
-
-                   pstore.sort();
-
-                   var groupingFeature = Ext.create('Ext.grid.feature.Grouping',{
-                       enableGroupingMenu: false,
-                       groupHeaderTpl: '<tpl if="groupValue">Optional</tpl><tpl if="!groupValue">Required</tpl>'
-                   });
-
-                   sections.push({
-                       xtype: 'gridpanel',
-                       title: 'Parameters',
-                       features: [groupingFeature],
-                       store: pstore,
-                       viewConfig: {
-                           trackOver: false,
-                           stripeRows: true
-                       },
-                       columns: [
-                           {
-                               header: 'Name',
-                               dataIndex: 'name',
-                               flex: 1
-                           },
-                           {
-                               header: 'Type',
-                               dataIndex: 'type',
-                               renderer: render_type,
-                               flex: 1
-                           },
-                           {
-                               header: 'Default',
-                               dataIndex: 'default',
-                               flex: 1
-                           },
-                           {
-                               header: 'Format',
-                               dataIndex: 'type',
-                               renderer: render_format,
-                               flex: 2
-                           },
-                           {
-                               header: 'Description',
-                               dataIndex: 'description',
-                               renderer: render_description,
-                               flex: 6
-                           }
-                       ]
-                   });
-
-               }
-
-               if (info.returns) {
-
-                   var retinf = info.returns;
-                   var rtype = retinf.type;
-                   if (!rtype && retinf.items)
-                       rtype = 'array';
-                   if (!rtype)
-                       rtype = 'object';
-
-                   var rpstore = Ext.create('Ext.data.Store', {
-                       model: 'pmx-param-schema',
-                       proxy: {
-                           type: 'memory'
-                       },
-                       groupField: 'optional',
-                       sorters: [
-                           {
-                               property: 'name',
-                               direction: 'ASC'
-                          }
-                       ]
-                   });
-
-                   var properties;
-                   if (rtype === 'array' && retinf.items.properties) {
-                       properties = retinf.items.properties;
-                   }
-
-                   if (rtype === 'object' && retinf.properties) {
-                       properties = retinf.properties;
-                   }
-
-                   Ext.Object.each(properties, function(name, pdef) {
-                       pdef.name = name;
-                       rpstore.add(pdef);
-                   });
-
-                   rpstore.sort();
-
-                   var groupingFeature = Ext.create('Ext.grid.feature.Grouping',{
-                       enableGroupingMenu: false,
-                       groupHeaderTpl: '<tpl if="groupValue">Optional</tpl><tpl if="!groupValue">Obligatory</tpl>'
-                   });
-                   var returnhtml;
-                   if (retinf.items) {
-                       returnhtml = '<pre>items: ' + Ext.htmlEncode(JSON.stringify(retinf.items, null, 4)) + '</pre>';
-                   }
-
-                   if (retinf.properties) {
-                       returnhtml = returnhtml || '';
-                       returnhtml += '<pre>properties:' + Ext.htmlEncode(JSON.stringify(retinf.properties, null, 4)) + '</pre>';
-                   }
-
-                   var rawSection = Ext.create('Ext.panel.Panel', {
-                       bodyPadding: '0px 10px 10px 10px',
-                       html: returnhtml,
-                       hidden: true
-                   });
-
-                   sections.push({
-                       xtype: 'gridpanel',
-                       title: 'Returns: ' + rtype,
-                       features: [groupingFeature],
-                       store: rpstore,
-                       viewConfig: {
-                           trackOver: false,
-                           stripeRows: true
-                       },
-                   columns: [
-                       {
-                           header: 'Name',
-                           dataIndex: 'name',
-                           flex: 1
-                       },
-                       {
-                           header: 'Type',
-                           dataIndex: 'type',
-                           renderer: render_type,
-                           flex: 1
-                       },
-                       {
-                           header: 'Default',
-                           dataIndex: 'default',
-                           flex: 1
-                       },
-                       {
-                           header: 'Format',
-                           dataIndex: 'type',
-                           renderer: render_format,
-                           flex: 2
-                       },
-                       {
-                           header: 'Description',
-                           dataIndex: 'description',
-                           renderer: render_description,
-                           flex: 6
-                       }
-                   ],
-                   bbar: [
-                       {
-                           xtype: 'button',
-                           text: 'Show RAW',
-                           handler: function(btn) {
-                               rawSection.setVisible(!rawSection.isVisible());
-                               btn.setText(rawSection.isVisible() ? 'Hide RAW' : 'Show RAW');
-                           }}
-                   ]
-               });
-
-               sections.push(rawSection);
-
-
-               }
-
-               if (!data.path.match(/\/_upgrade_/)) {
-                   var permhtml = '';
-
-                   if (!info.permissions) {
-                       permhtml = "Root only.";
-                   } else {
-                       if (info.permissions.description) {
-                           permhtml += "<div style='white-space:pre-wrap;padding-bottom:10px;'>" +
-                               Ext.htmlEncode(info.permissions.description) + "</div>";
-                       }
-                       permhtml += permission_text(info.permissions);
-                   }
-
-                   if (info.allowtoken !== undefined && !info.allowtoken) {
-                       permhtml += "<br />This API endpoint is not available for API tokens."
-                   }
-
-                   sections.push({
-                       title: 'Required permissions',
-                       bodyPadding: 10,
-                       html: permhtml
-                   });
-               }
-
-               items.push({
-                   title: method,
-                   autoScroll: true,
-                   defaults: {
-                       border: false
-                   },
-                   items: sections
-               });
-           }
-       });
-
-       var ct = Ext.getCmp('docview');
-       ct.setTitle("Path: " +  real_path(data.path));
-       ct.removeAll(true);
-       ct.add(items);
-       ct.setActiveTab(0);
-    };
-
-    Ext.define('Ext.form.SearchField', {
-       extend: 'Ext.form.field.Text',
-       alias: 'widget.searchfield',
-
-       emptyText: 'Search...',
-
-       flex: 1,
-
-       inputType: 'search',
-       listeners: {
-           'change': function(){
-
-               var value = this.getValue();
-               if (!Ext.isEmpty(value)) {
-                   store.filter({
-                       property: 'path',
-                       value: value,
-                       anyMatch: true
-                   });
-               } else {
-                   store.clearFilter();
-               }
-           }
-       }
-    });
-
-    var tree = Ext.create('Ext.tree.Panel', {
-       title: 'Resource Tree',
-       tbar: [
-           {
-               xtype: 'searchfield',
-           }
-       ],
-       tools: [
-           {
-               type: 'expand',
-               tooltip: 'Expand all',
-               tooltipType: 'title',
-               callback: (tree) => tree.expandAll(),
-           },
-           {
-               type: 'collapse',
-               tooltip: 'Collapse all',
-               tooltipType: 'title',
-               callback: (tree) => tree.collapseAll(),
-           },
-       ],
-        store: store,
-       width: 200,
-        region: 'west',
-        split: true,
-        margins: '5 0 5 5',
-        rootVisible: false,
-       listeners: {
-           selectionchange: function(v, selections) {
-               if (!selections[0])
-                   return;
-               var rec = selections[0];
-               render_docu(rec.data);
-               location.hash = '#' + rec.data.path;
-           }
-       }
-    });
-
-    Ext.create('Ext.container.Viewport', {
-       layout: 'border',
-       renderTo: Ext.getBody(),
-       items: [
-           tree,
-           {
-               xtype: 'tabpanel',
-               title: 'Documentation',
-               id: 'docview',
-               region: 'center',
-               margins: '5 5 5 0',
-               layout: 'fit',
-               items: []
-           }
-       ]
-    });
-
-    var deepLink = function() {
-       var path = window.location.hash.substring(1).replace(/\/\s*$/, '')
-       var endpoint = store.findNode('path', path);
-
-       if (endpoint) {
-           tree.getSelectionModel().select(endpoint);
-           tree.expandPath(endpoint.getPath());
-           render_docu(endpoint.data);
-       }
-    }
-    window.onhashchange = deepLink;
-
-    deepLink();
-
-});
diff --git a/src/api-viewer/APIViewer.js b/src/api-viewer/APIViewer.js
new file mode 100644 (file)
index 0000000..c2864ed
--- /dev/null
@@ -0,0 +1,529 @@
+// avoid errors when running without development tools
+if (!Ext.isDefined(Ext.global.console)) {
+    var console = {
+        dir: function() {},
+        log: function() {}
+    };
+}
+
+Ext.onReady(function() {
+
+    Ext.define('pmx-param-schema', {
+        extend: 'Ext.data.Model',
+        fields:  [
+           'name', 'type', 'typetext', 'description', 'verbose_description',
+           'enum', 'minimum', 'maximum', 'minLength', 'maxLength',
+           'pattern', 'title', 'requires', 'format', 'default',
+           'disallow', 'extends', 'links',
+           {
+               name: 'optional',
+               type: 'boolean'
+           }
+       ]
+    });
+
+    var store = Ext.define('pmx-updated-treestore', {
+       extend: 'Ext.data.TreeStore',
+       model: Ext.define('pmx-api-doc', {
+            extend: 'Ext.data.Model',
+            fields:  [
+               'path', 'info', 'text',
+           ]
+       }),
+        proxy: {
+            type: 'memory',
+            data: pmxapi
+        },
+        sorters: [{
+            property: 'leaf',
+            direction: 'ASC'
+        }, {
+            property: 'text',
+            direction: 'ASC'
+       }],
+       filterer: 'bottomup',
+       doFilter: function(node) {
+           this.filterNodes(node, this.getFilters().getFilterFn(), true);
+       },
+
+       filterNodes: function(node, filterFn, parentVisible) {
+           var me = this,
+               bottomUpFiltering = me.filterer === 'bottomup',
+               match = filterFn(node) && parentVisible || (node.isRoot() && !me.getRootVisible()),
+               childNodes = node.childNodes,
+               len = childNodes && childNodes.length, i, matchingChildren;
+
+           if (len) {
+               for (i = 0; i < len; ++i) {
+                   matchingChildren = me.filterNodes(childNodes[i], filterFn, match || bottomUpFiltering) || matchingChildren;
+               }
+               if (bottomUpFiltering) {
+                   match = matchingChildren || match;
+               }
+           }
+
+           node.set("visible", match, me._silentOptions);
+           return match;
+       },
+
+    }).create();
+
+    var render_description = function(value, metaData, record) {
+       var pdef = record.data;
+
+       value = pdef.verbose_description || value;
+
+       // TODO: try to render asciidoc correctly
+
+       metaData.style = 'white-space:pre-wrap;'
+
+       return Ext.htmlEncode(value);
+    };
+
+    var render_type = function(value, metaData, record) {
+       var pdef = record.data;
+
+       return pdef['enum'] ? 'enum' : (pdef.type || 'string');
+    };
+
+    let render_simple_format = function(pdef, type_fallback) {
+       if (pdef.typetext)
+           return pdef.typetext;
+
+       if (pdef['enum'])
+           return pdef['enum'].join(' | ');
+
+       if (pdef.format)
+           return pdef.format;
+
+       if (pdef.pattern)
+           return pdef.pattern;
+
+       if (pdef.type === 'boolean')
+           return `<true|false>`;
+
+       if (type_fallback && pdef.type)
+           return `<${pdef.type}>`;
+
+       return;
+    };
+
+    let render_format = function(value, metaData, record) {
+       let pdef = record.data;
+
+       metaData.style = 'white-space:normal;'
+
+       if (pdef.type === 'array' && pdef.items) {
+           let format = render_simple_format(pdef.items, true);
+           return `[${Ext.htmlEncode(format)}, ...]`;
+       }
+
+       return Ext.htmlEncode(render_simple_format(pdef) || '');
+    };
+
+    var real_path = function(path) {
+       return path.replace(/^.*\/_upgrade_(\/)?/, "/");
+    };
+
+    var permission_text = function(permission) {
+       let permhtml = "";
+
+       if (permission.user) {
+           if (!permission.description) {
+               if (permission.user === 'world') {
+                   permhtml += "Accessible without any authentication.";
+               } else if (permission.user === 'all') {
+                   permhtml += "Accessible by all authenticated users.";
+               } else {
+                   permhtml += 'Onyl accessible by user "' +
+                       permission.user + '"';
+               }
+           }
+       } else if (permission.check) {
+           permhtml += "<pre>Check: " +
+               Ext.htmlEncode(Ext.JSON.encode(permission.check))  + "</pre>";
+       } else if (permission.userParam) {
+           permhtml += `<div>Check if user matches parameter '${permission.userParam}'`;
+       } else if (permission.or) {
+           permhtml += "<div>Or<div style='padding-left: 10px;'>";
+           Ext.Array.each(permission.or, function(sub_permission) {
+               permhtml += permission_text(sub_permission);
+           })
+           permhtml += "</div></div>";
+       } else if (permission.and) {
+           permhtml += "<div>And<div style='padding-left: 10px;'>";
+           Ext.Array.each(permission.and, function(sub_permission) {
+               permhtml += permission_text(sub_permission);
+           })
+           permhtml += "</div></div>";
+       } else {
+           //console.log(permission);
+           permhtml += "Unknown syntax!";
+       }
+
+       return permhtml;
+    };
+
+    var render_docu = function(data) {
+       var md = data.info;
+
+       // console.dir(data);
+
+       var items = [];
+
+       var clicmdhash = {
+           GET: 'get',
+           POST: 'create',
+           PUT: 'set',
+           DELETE: 'delete'
+       };
+
+       Ext.Array.each(['GET', 'POST', 'PUT', 'DELETE'], function(method) {
+           var info = md[method];
+           if (info) {
+
+               var usage = "";
+
+               usage += "<table><tr><td>HTTP:&nbsp;&nbsp;&nbsp;</td><td>"
+                   + method + " " + real_path("/api2/json" + data.path) + "</td></tr>";
+
+               if (typeof cliusage === 'function') {
+                   usage += cliusage(method, real_path(data.path));
+               }
+
+               var sections = [
+                   {
+                       title: 'Description',
+                       html: Ext.htmlEncode(info.description),
+                       bodyPadding: 10
+                   },
+                   {
+                       title: 'Usage',
+                       html: usage,
+                       bodyPadding: 10
+                   }
+               ];
+
+               if (info.parameters && info.parameters.properties) {
+
+                   var pstore = Ext.create('Ext.data.Store', {
+                       model: 'pmx-param-schema',
+                       proxy: {
+                           type: 'memory'
+                       },
+                       groupField: 'optional',
+                       sorters: [
+                           {
+                               property: 'name',
+                               direction: 'ASC'
+                           }
+                       ]
+                   });
+
+                   Ext.Object.each(info.parameters.properties, function(name, pdef) {
+                       pdef.name = name;
+                       pstore.add(pdef);
+                   });
+
+                   pstore.sort();
+
+                   var groupingFeature = Ext.create('Ext.grid.feature.Grouping',{
+                       enableGroupingMenu: false,
+                       groupHeaderTpl: '<tpl if="groupValue">Optional</tpl><tpl if="!groupValue">Required</tpl>'
+                   });
+
+                   sections.push({
+                       xtype: 'gridpanel',
+                       title: 'Parameters',
+                       features: [groupingFeature],
+                       store: pstore,
+                       viewConfig: {
+                           trackOver: false,
+                           stripeRows: true
+                       },
+                       columns: [
+                           {
+                               header: 'Name',
+                               dataIndex: 'name',
+                               flex: 1
+                           },
+                           {
+                               header: 'Type',
+                               dataIndex: 'type',
+                               renderer: render_type,
+                               flex: 1
+                           },
+                           {
+                               header: 'Default',
+                               dataIndex: 'default',
+                               flex: 1
+                           },
+                           {
+                               header: 'Format',
+                               dataIndex: 'type',
+                               renderer: render_format,
+                               flex: 2
+                           },
+                           {
+                               header: 'Description',
+                               dataIndex: 'description',
+                               renderer: render_description,
+                               flex: 6
+                           }
+                       ]
+                   });
+
+               }
+
+               if (info.returns) {
+
+                   var retinf = info.returns;
+                   var rtype = retinf.type;
+                   if (!rtype && retinf.items)
+                       rtype = 'array';
+                   if (!rtype)
+                       rtype = 'object';
+
+                   var rpstore = Ext.create('Ext.data.Store', {
+                       model: 'pmx-param-schema',
+                       proxy: {
+                           type: 'memory'
+                       },
+                       groupField: 'optional',
+                       sorters: [
+                           {
+                               property: 'name',
+                               direction: 'ASC'
+                          }
+                       ]
+                   });
+
+                   var properties;
+                   if (rtype === 'array' && retinf.items.properties) {
+                       properties = retinf.items.properties;
+                   }
+
+                   if (rtype === 'object' && retinf.properties) {
+                       properties = retinf.properties;
+                   }
+
+                   Ext.Object.each(properties, function(name, pdef) {
+                       pdef.name = name;
+                       rpstore.add(pdef);
+                   });
+
+                   rpstore.sort();
+
+                   var groupingFeature = Ext.create('Ext.grid.feature.Grouping',{
+                       enableGroupingMenu: false,
+                       groupHeaderTpl: '<tpl if="groupValue">Optional</tpl><tpl if="!groupValue">Obligatory</tpl>'
+                   });
+                   var returnhtml;
+                   if (retinf.items) {
+                       returnhtml = '<pre>items: ' + Ext.htmlEncode(JSON.stringify(retinf.items, null, 4)) + '</pre>';
+                   }
+
+                   if (retinf.properties) {
+                       returnhtml = returnhtml || '';
+                       returnhtml += '<pre>properties:' + Ext.htmlEncode(JSON.stringify(retinf.properties, null, 4)) + '</pre>';
+                   }
+
+                   var rawSection = Ext.create('Ext.panel.Panel', {
+                       bodyPadding: '0px 10px 10px 10px',
+                       html: returnhtml,
+                       hidden: true
+                   });
+
+                   sections.push({
+                       xtype: 'gridpanel',
+                       title: 'Returns: ' + rtype,
+                       features: [groupingFeature],
+                       store: rpstore,
+                       viewConfig: {
+                           trackOver: false,
+                           stripeRows: true
+                       },
+                   columns: [
+                       {
+                           header: 'Name',
+                           dataIndex: 'name',
+                           flex: 1
+                       },
+                       {
+                           header: 'Type',
+                           dataIndex: 'type',
+                           renderer: render_type,
+                           flex: 1
+                       },
+                       {
+                           header: 'Default',
+                           dataIndex: 'default',
+                           flex: 1
+                       },
+                       {
+                           header: 'Format',
+                           dataIndex: 'type',
+                           renderer: render_format,
+                           flex: 2
+                       },
+                       {
+                           header: 'Description',
+                           dataIndex: 'description',
+                           renderer: render_description,
+                           flex: 6
+                       }
+                   ],
+                   bbar: [
+                       {
+                           xtype: 'button',
+                           text: 'Show RAW',
+                           handler: function(btn) {
+                               rawSection.setVisible(!rawSection.isVisible());
+                               btn.setText(rawSection.isVisible() ? 'Hide RAW' : 'Show RAW');
+                           }}
+                   ]
+               });
+
+               sections.push(rawSection);
+
+
+               }
+
+               if (!data.path.match(/\/_upgrade_/)) {
+                   var permhtml = '';
+
+                   if (!info.permissions) {
+                       permhtml = "Root only.";
+                   } else {
+                       if (info.permissions.description) {
+                           permhtml += "<div style='white-space:pre-wrap;padding-bottom:10px;'>" +
+                               Ext.htmlEncode(info.permissions.description) + "</div>";
+                       }
+                       permhtml += permission_text(info.permissions);
+                   }
+
+                   if (info.allowtoken !== undefined && !info.allowtoken) {
+                       permhtml += "<br />This API endpoint is not available for API tokens."
+                   }
+
+                   sections.push({
+                       title: 'Required permissions',
+                       bodyPadding: 10,
+                       html: permhtml
+                   });
+               }
+
+               items.push({
+                   title: method,
+                   autoScroll: true,
+                   defaults: {
+                       border: false
+                   },
+                   items: sections
+               });
+           }
+       });
+
+       var ct = Ext.getCmp('docview');
+       ct.setTitle("Path: " +  real_path(data.path));
+       ct.removeAll(true);
+       ct.add(items);
+       ct.setActiveTab(0);
+    };
+
+    Ext.define('Ext.form.SearchField', {
+       extend: 'Ext.form.field.Text',
+       alias: 'widget.searchfield',
+
+       emptyText: 'Search...',
+
+       flex: 1,
+
+       inputType: 'search',
+       listeners: {
+           'change': function(){
+
+               var value = this.getValue();
+               if (!Ext.isEmpty(value)) {
+                   store.filter({
+                       property: 'path',
+                       value: value,
+                       anyMatch: true
+                   });
+               } else {
+                   store.clearFilter();
+               }
+           }
+       }
+    });
+
+    var tree = Ext.create('Ext.tree.Panel', {
+       title: 'Resource Tree',
+       tbar: [
+           {
+               xtype: 'searchfield',
+           }
+       ],
+       tools: [
+           {
+               type: 'expand',
+               tooltip: 'Expand all',
+               tooltipType: 'title',
+               callback: (tree) => tree.expandAll(),
+           },
+           {
+               type: 'collapse',
+               tooltip: 'Collapse all',
+               tooltipType: 'title',
+               callback: (tree) => tree.collapseAll(),
+           },
+       ],
+        store: store,
+       width: 200,
+        region: 'west',
+        split: true,
+        margins: '5 0 5 5',
+        rootVisible: false,
+       listeners: {
+           selectionchange: function(v, selections) {
+               if (!selections[0])
+                   return;
+               var rec = selections[0];
+               render_docu(rec.data);
+               location.hash = '#' + rec.data.path;
+           }
+       }
+    });
+
+    Ext.create('Ext.container.Viewport', {
+       layout: 'border',
+       renderTo: Ext.getBody(),
+       items: [
+           tree,
+           {
+               xtype: 'tabpanel',
+               title: 'Documentation',
+               id: 'docview',
+               region: 'center',
+               margins: '5 5 5 0',
+               layout: 'fit',
+               items: []
+           }
+       ]
+    });
+
+    var deepLink = function() {
+       var path = window.location.hash.substring(1).replace(/\/\s*$/, '')
+       var endpoint = store.findNode('path', path);
+
+       if (endpoint) {
+           tree.getSelectionModel().select(endpoint);
+           tree.expandPath(endpoint.getPath());
+           render_docu(endpoint.data);
+       }
+    }
+    window.onhashchange = deepLink;
+
+    deepLink();
+
+});