]> git.proxmox.com Git - proxmox-widget-toolkit.git/blob - src/api-viewer/APIViewer.js
don't shout: s/APIVIEWER/APIViewer/
[proxmox-widget-toolkit.git] / src / api-viewer / APIViewer.js
1 // avoid errors when running without development tools
2 if (!Ext.isDefined(Ext.global.console)) {
3 var console = {
4 dir: function() {},
5 log: function() {}
6 };
7 }
8
9 Ext.onReady(function() {
10
11 Ext.define('pmx-param-schema', {
12 extend: 'Ext.data.Model',
13 fields: [
14 'name', 'type', 'typetext', 'description', 'verbose_description',
15 'enum', 'minimum', 'maximum', 'minLength', 'maxLength',
16 'pattern', 'title', 'requires', 'format', 'default',
17 'disallow', 'extends', 'links',
18 {
19 name: 'optional',
20 type: 'boolean'
21 }
22 ]
23 });
24
25 var store = Ext.define('pmx-updated-treestore', {
26 extend: 'Ext.data.TreeStore',
27 model: Ext.define('pmx-api-doc', {
28 extend: 'Ext.data.Model',
29 fields: [
30 'path', 'info', 'text',
31 ]
32 }),
33 proxy: {
34 type: 'memory',
35 data: pmxapi
36 },
37 sorters: [{
38 property: 'leaf',
39 direction: 'ASC'
40 }, {
41 property: 'text',
42 direction: 'ASC'
43 }],
44 filterer: 'bottomup',
45 doFilter: function(node) {
46 this.filterNodes(node, this.getFilters().getFilterFn(), true);
47 },
48
49 filterNodes: function(node, filterFn, parentVisible) {
50 var me = this,
51 bottomUpFiltering = me.filterer === 'bottomup',
52 match = filterFn(node) && parentVisible || (node.isRoot() && !me.getRootVisible()),
53 childNodes = node.childNodes,
54 len = childNodes && childNodes.length, i, matchingChildren;
55
56 if (len) {
57 for (i = 0; i < len; ++i) {
58 matchingChildren = me.filterNodes(childNodes[i], filterFn, match || bottomUpFiltering) || matchingChildren;
59 }
60 if (bottomUpFiltering) {
61 match = matchingChildren || match;
62 }
63 }
64
65 node.set("visible", match, me._silentOptions);
66 return match;
67 },
68
69 }).create();
70
71 var render_description = function(value, metaData, record) {
72 var pdef = record.data;
73
74 value = pdef.verbose_description || value;
75
76 // TODO: try to render asciidoc correctly
77
78 metaData.style = 'white-space:pre-wrap;'
79
80 return Ext.htmlEncode(value);
81 };
82
83 var render_type = function(value, metaData, record) {
84 var pdef = record.data;
85
86 return pdef['enum'] ? 'enum' : (pdef.type || 'string');
87 };
88
89 let render_simple_format = function(pdef, type_fallback) {
90 if (pdef.typetext)
91 return pdef.typetext;
92
93 if (pdef['enum'])
94 return pdef['enum'].join(' | ');
95
96 if (pdef.format)
97 return pdef.format;
98
99 if (pdef.pattern)
100 return pdef.pattern;
101
102 if (pdef.type === 'boolean')
103 return `<true|false>`;
104
105 if (type_fallback && pdef.type)
106 return `<${pdef.type}>`;
107
108 return;
109 };
110
111 let render_format = function(value, metaData, record) {
112 let pdef = record.data;
113
114 metaData.style = 'white-space:normal;'
115
116 if (pdef.type === 'array' && pdef.items) {
117 let format = render_simple_format(pdef.items, true);
118 return `[${Ext.htmlEncode(format)}, ...]`;
119 }
120
121 return Ext.htmlEncode(render_simple_format(pdef) || '');
122 };
123
124 var real_path = function(path) {
125 return path.replace(/^.*\/_upgrade_(\/)?/, "/");
126 };
127
128 var permission_text = function(permission) {
129 let permhtml = "";
130
131 if (permission.user) {
132 if (!permission.description) {
133 if (permission.user === 'world') {
134 permhtml += "Accessible without any authentication.";
135 } else if (permission.user === 'all') {
136 permhtml += "Accessible by all authenticated users.";
137 } else {
138 permhtml += 'Onyl accessible by user "' +
139 permission.user + '"';
140 }
141 }
142 } else if (permission.check) {
143 permhtml += "<pre>Check: " +
144 Ext.htmlEncode(Ext.JSON.encode(permission.check)) + "</pre>";
145 } else if (permission.userParam) {
146 permhtml += `<div>Check if user matches parameter '${permission.userParam}'`;
147 } else if (permission.or) {
148 permhtml += "<div>Or<div style='padding-left: 10px;'>";
149 Ext.Array.each(permission.or, function(sub_permission) {
150 permhtml += permission_text(sub_permission);
151 })
152 permhtml += "</div></div>";
153 } else if (permission.and) {
154 permhtml += "<div>And<div style='padding-left: 10px;'>";
155 Ext.Array.each(permission.and, function(sub_permission) {
156 permhtml += permission_text(sub_permission);
157 })
158 permhtml += "</div></div>";
159 } else {
160 //console.log(permission);
161 permhtml += "Unknown syntax!";
162 }
163
164 return permhtml;
165 };
166
167 var render_docu = function(data) {
168 var md = data.info;
169
170 // console.dir(data);
171
172 var items = [];
173
174 var clicmdhash = {
175 GET: 'get',
176 POST: 'create',
177 PUT: 'set',
178 DELETE: 'delete'
179 };
180
181 Ext.Array.each(['GET', 'POST', 'PUT', 'DELETE'], function(method) {
182 var info = md[method];
183 if (info) {
184
185 var usage = "";
186
187 usage += "<table><tr><td>HTTP:&nbsp;&nbsp;&nbsp;</td><td>"
188 + method + " " + real_path("/api2/json" + data.path) + "</td></tr>";
189
190 if (typeof cliusage === 'function') {
191 usage += cliusage(method, real_path(data.path));
192 }
193
194 var sections = [
195 {
196 title: 'Description',
197 html: Ext.htmlEncode(info.description),
198 bodyPadding: 10
199 },
200 {
201 title: 'Usage',
202 html: usage,
203 bodyPadding: 10
204 }
205 ];
206
207 if (info.parameters && info.parameters.properties) {
208
209 var pstore = Ext.create('Ext.data.Store', {
210 model: 'pmx-param-schema',
211 proxy: {
212 type: 'memory'
213 },
214 groupField: 'optional',
215 sorters: [
216 {
217 property: 'name',
218 direction: 'ASC'
219 }
220 ]
221 });
222
223 Ext.Object.each(info.parameters.properties, function(name, pdef) {
224 pdef.name = name;
225 pstore.add(pdef);
226 });
227
228 pstore.sort();
229
230 var groupingFeature = Ext.create('Ext.grid.feature.Grouping',{
231 enableGroupingMenu: false,
232 groupHeaderTpl: '<tpl if="groupValue">Optional</tpl><tpl if="!groupValue">Required</tpl>'
233 });
234
235 sections.push({
236 xtype: 'gridpanel',
237 title: 'Parameters',
238 features: [groupingFeature],
239 store: pstore,
240 viewConfig: {
241 trackOver: false,
242 stripeRows: true
243 },
244 columns: [
245 {
246 header: 'Name',
247 dataIndex: 'name',
248 flex: 1
249 },
250 {
251 header: 'Type',
252 dataIndex: 'type',
253 renderer: render_type,
254 flex: 1
255 },
256 {
257 header: 'Default',
258 dataIndex: 'default',
259 flex: 1
260 },
261 {
262 header: 'Format',
263 dataIndex: 'type',
264 renderer: render_format,
265 flex: 2
266 },
267 {
268 header: 'Description',
269 dataIndex: 'description',
270 renderer: render_description,
271 flex: 6
272 }
273 ]
274 });
275
276 }
277
278 if (info.returns) {
279
280 var retinf = info.returns;
281 var rtype = retinf.type;
282 if (!rtype && retinf.items)
283 rtype = 'array';
284 if (!rtype)
285 rtype = 'object';
286
287 var rpstore = Ext.create('Ext.data.Store', {
288 model: 'pmx-param-schema',
289 proxy: {
290 type: 'memory'
291 },
292 groupField: 'optional',
293 sorters: [
294 {
295 property: 'name',
296 direction: 'ASC'
297 }
298 ]
299 });
300
301 var properties;
302 if (rtype === 'array' && retinf.items.properties) {
303 properties = retinf.items.properties;
304 }
305
306 if (rtype === 'object' && retinf.properties) {
307 properties = retinf.properties;
308 }
309
310 Ext.Object.each(properties, function(name, pdef) {
311 pdef.name = name;
312 rpstore.add(pdef);
313 });
314
315 rpstore.sort();
316
317 var groupingFeature = Ext.create('Ext.grid.feature.Grouping',{
318 enableGroupingMenu: false,
319 groupHeaderTpl: '<tpl if="groupValue">Optional</tpl><tpl if="!groupValue">Obligatory</tpl>'
320 });
321 var returnhtml;
322 if (retinf.items) {
323 returnhtml = '<pre>items: ' + Ext.htmlEncode(JSON.stringify(retinf.items, null, 4)) + '</pre>';
324 }
325
326 if (retinf.properties) {
327 returnhtml = returnhtml || '';
328 returnhtml += '<pre>properties:' + Ext.htmlEncode(JSON.stringify(retinf.properties, null, 4)) + '</pre>';
329 }
330
331 var rawSection = Ext.create('Ext.panel.Panel', {
332 bodyPadding: '0px 10px 10px 10px',
333 html: returnhtml,
334 hidden: true
335 });
336
337 sections.push({
338 xtype: 'gridpanel',
339 title: 'Returns: ' + rtype,
340 features: [groupingFeature],
341 store: rpstore,
342 viewConfig: {
343 trackOver: false,
344 stripeRows: true
345 },
346 columns: [
347 {
348 header: 'Name',
349 dataIndex: 'name',
350 flex: 1
351 },
352 {
353 header: 'Type',
354 dataIndex: 'type',
355 renderer: render_type,
356 flex: 1
357 },
358 {
359 header: 'Default',
360 dataIndex: 'default',
361 flex: 1
362 },
363 {
364 header: 'Format',
365 dataIndex: 'type',
366 renderer: render_format,
367 flex: 2
368 },
369 {
370 header: 'Description',
371 dataIndex: 'description',
372 renderer: render_description,
373 flex: 6
374 }
375 ],
376 bbar: [
377 {
378 xtype: 'button',
379 text: 'Show RAW',
380 handler: function(btn) {
381 rawSection.setVisible(!rawSection.isVisible());
382 btn.setText(rawSection.isVisible() ? 'Hide RAW' : 'Show RAW');
383 }}
384 ]
385 });
386
387 sections.push(rawSection);
388
389
390 }
391
392 if (!data.path.match(/\/_upgrade_/)) {
393 var permhtml = '';
394
395 if (!info.permissions) {
396 permhtml = "Root only.";
397 } else {
398 if (info.permissions.description) {
399 permhtml += "<div style='white-space:pre-wrap;padding-bottom:10px;'>" +
400 Ext.htmlEncode(info.permissions.description) + "</div>";
401 }
402 permhtml += permission_text(info.permissions);
403 }
404
405 if (info.allowtoken !== undefined && !info.allowtoken) {
406 permhtml += "<br />This API endpoint is not available for API tokens."
407 }
408
409 sections.push({
410 title: 'Required permissions',
411 bodyPadding: 10,
412 html: permhtml
413 });
414 }
415
416 items.push({
417 title: method,
418 autoScroll: true,
419 defaults: {
420 border: false
421 },
422 items: sections
423 });
424 }
425 });
426
427 var ct = Ext.getCmp('docview');
428 ct.setTitle("Path: " + real_path(data.path));
429 ct.removeAll(true);
430 ct.add(items);
431 ct.setActiveTab(0);
432 };
433
434 Ext.define('Ext.form.SearchField', {
435 extend: 'Ext.form.field.Text',
436 alias: 'widget.searchfield',
437
438 emptyText: 'Search...',
439
440 flex: 1,
441
442 inputType: 'search',
443 listeners: {
444 'change': function(){
445
446 var value = this.getValue();
447 if (!Ext.isEmpty(value)) {
448 store.filter({
449 property: 'path',
450 value: value,
451 anyMatch: true
452 });
453 } else {
454 store.clearFilter();
455 }
456 }
457 }
458 });
459
460 var tree = Ext.create('Ext.tree.Panel', {
461 title: 'Resource Tree',
462 tbar: [
463 {
464 xtype: 'searchfield',
465 }
466 ],
467 tools: [
468 {
469 type: 'expand',
470 tooltip: 'Expand all',
471 tooltipType: 'title',
472 callback: (tree) => tree.expandAll(),
473 },
474 {
475 type: 'collapse',
476 tooltip: 'Collapse all',
477 tooltipType: 'title',
478 callback: (tree) => tree.collapseAll(),
479 },
480 ],
481 store: store,
482 width: 200,
483 region: 'west',
484 split: true,
485 margins: '5 0 5 5',
486 rootVisible: false,
487 listeners: {
488 selectionchange: function(v, selections) {
489 if (!selections[0])
490 return;
491 var rec = selections[0];
492 render_docu(rec.data);
493 location.hash = '#' + rec.data.path;
494 }
495 }
496 });
497
498 Ext.create('Ext.container.Viewport', {
499 layout: 'border',
500 renderTo: Ext.getBody(),
501 items: [
502 tree,
503 {
504 xtype: 'tabpanel',
505 title: 'Documentation',
506 id: 'docview',
507 region: 'center',
508 margins: '5 5 5 0',
509 layout: 'fit',
510 items: []
511 }
512 ]
513 });
514
515 var deepLink = function() {
516 var path = window.location.hash.substring(1).replace(/\/\s*$/, '')
517 var endpoint = store.findNode('path', path);
518
519 if (endpoint) {
520 tree.getSelectionModel().select(endpoint);
521 tree.expandPath(endpoint.getPath());
522 render_docu(endpoint.data);
523 }
524 }
525 window.onhashchange = deepLink;
526
527 deepLink();
528
529 });