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