]> git.proxmox.com Git - proxmox-widget-toolkit.git/blame - src/api-viewer/APIViewer.js
api-viewer: eslint fixes, code cleanups
[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',
10 'disallow', 'extends', 'links',
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
82 let render_simple_format = function(pdef, type_fallback) {
ac4fa7fe
TL
83 if (pdef.typetext) {
84 return pdef.typetext;
85 }
86 if (pdef.enum) {
87 return pdef.enum.join(' | ');
88 }
89 if (pdef.format) {
90 return pdef.format;
91 }
92 if (pdef.pattern) {
93 return pdef.pattern;
94 }
95 if (pdef.type === 'boolean') {
96 return `<true|false>`;
97 }
98 if (type_fallback && pdef.type) {
99 return `<${pdef.type}>`;
100 }
101 return '';
4ae75df3
DC
102 };
103
104 let render_format = function(value, metaData, record) {
105 let pdef = record.data;
106
52428d60 107 metaData.style = 'white-space:normal;';
4ae75df3
DC
108
109 if (pdef.type === 'array' && pdef.items) {
110 let format = render_simple_format(pdef.items, true);
111 return `[${Ext.htmlEncode(format)}, ...]`;
112 }
113
ac4fa7fe 114 return Ext.htmlEncode(render_simple_format(pdef));
4ae75df3
DC
115 };
116
52428d60 117 let real_path = function(path) {
4ae75df3
DC
118 return path.replace(/^.*\/_upgrade_(\/)?/, "/");
119 };
120
52428d60 121 let permission_text = function(permission) {
4ae75df3
DC
122 let permhtml = "";
123
124 if (permission.user) {
125 if (!permission.description) {
126 if (permission.user === 'world') {
127 permhtml += "Accessible without any authentication.";
128 } else if (permission.user === 'all') {
129 permhtml += "Accessible by all authenticated users.";
130 } else {
ac4fa7fe 131 permhtml += `Only accessible by user "${permission.user}"`;
4ae75df3
DC
132 }
133 }
134 } else if (permission.check) {
ac4fa7fe 135 permhtml += `<pre>Check: ${Ext.htmlEncode(JSON.stringify(permission.check))}</pre>`;
4ae75df3
DC
136 } else if (permission.userParam) {
137 permhtml += `<div>Check if user matches parameter '${permission.userParam}'`;
138 } else if (permission.or) {
139 permhtml += "<div>Or<div style='padding-left: 10px;'>";
ac4fa7fe 140 permhtml += permission.or.map(v => permission_text(v)).join('');
4ae75df3
DC
141 permhtml += "</div></div>";
142 } else if (permission.and) {
143 permhtml += "<div>And<div style='padding-left: 10px;'>";
ac4fa7fe 144 permhtml += permission.and.map(v => permission_text(v)).join('');
4ae75df3
DC
145 permhtml += "</div></div>";
146 } else {
4ae75df3
DC
147 permhtml += "Unknown syntax!";
148 }
149
150 return permhtml;
151 };
152
52428d60
TL
153 let render_docu = function(data) {
154 let md = data.info;
4ae75df3 155
52428d60 156 let items = [];
4ae75df3 157
4ae75df3 158 Ext.Array.each(['GET', 'POST', 'PUT', 'DELETE'], function(method) {
52428d60 159 let info = md[method];
4ae75df3 160 if (info) {
ac4fa7fe
TL
161 let endpoint = real_path(data.path);
162 let usage = `<table><tr><td>HTTP:&nbsp;&nbsp;&nbsp;</td><td>`;
163 usage += `${method} /api2/json/${endpoint}</td></tr>`;
4ae75df3 164
ac4fa7fe
TL
165 if (typeof cliUsageRenderer === 'function') {
166 usage += cliUsageRenderer(method, endpoint); // eslint-disable-line no-undef
4ae75df3
DC
167 }
168
52428d60 169 let sections = [
4ae75df3
DC
170 {
171 title: 'Description',
172 html: Ext.htmlEncode(info.description),
52428d60 173 bodyPadding: 10,
4ae75df3
DC
174 },
175 {
176 title: 'Usage',
177 html: usage,
52428d60
TL
178 bodyPadding: 10,
179 },
4ae75df3
DC
180 ];
181
182 if (info.parameters && info.parameters.properties) {
52428d60 183 let pstore = Ext.create('Ext.data.Store', {
4ae75df3
DC
184 model: 'pmx-param-schema',
185 proxy: {
52428d60 186 type: 'memory',
4ae75df3
DC
187 },
188 groupField: 'optional',
189 sorters: [
190 {
191 property: 'name',
52428d60
TL
192 direction: 'ASC',
193 },
194 ],
4ae75df3
DC
195 });
196
197 Ext.Object.each(info.parameters.properties, function(name, pdef) {
198 pdef.name = name;
199 pstore.add(pdef);
200 });
201
202 pstore.sort();
203
52428d60 204 let groupingFeature = Ext.create('Ext.grid.feature.Grouping', {
4ae75df3 205 enableGroupingMenu: false,
52428d60 206 groupHeaderTpl: '<tpl if="groupValue">Optional</tpl><tpl if="!groupValue">Required</tpl>',
4ae75df3
DC
207 });
208
209 sections.push({
210 xtype: 'gridpanel',
211 title: 'Parameters',
212 features: [groupingFeature],
213 store: pstore,
214 viewConfig: {
215 trackOver: false,
52428d60 216 stripeRows: true,
4ae75df3
DC
217 },
218 columns: [
219 {
220 header: 'Name',
221 dataIndex: 'name',
52428d60 222 flex: 1,
4ae75df3
DC
223 },
224 {
225 header: 'Type',
226 dataIndex: 'type',
227 renderer: render_type,
52428d60 228 flex: 1,
4ae75df3
DC
229 },
230 {
231 header: 'Default',
232 dataIndex: 'default',
52428d60 233 flex: 1,
4ae75df3
DC
234 },
235 {
236 header: 'Format',
237 dataIndex: 'type',
238 renderer: render_format,
52428d60 239 flex: 2,
4ae75df3
DC
240 },
241 {
242 header: 'Description',
243 dataIndex: 'description',
244 renderer: render_description,
52428d60
TL
245 flex: 6,
246 },
247 ],
4ae75df3 248 });
4ae75df3
DC
249 }
250
251 if (info.returns) {
52428d60
TL
252 let retinf = info.returns;
253 let rtype = retinf.type;
254 if (!rtype && retinf.items) {rtype = 'array';}
255 if (!rtype) {rtype = 'object';}
4ae75df3 256
52428d60 257 let rpstore = Ext.create('Ext.data.Store', {
4ae75df3
DC
258 model: 'pmx-param-schema',
259 proxy: {
52428d60 260 type: 'memory',
4ae75df3
DC
261 },
262 groupField: 'optional',
263 sorters: [
264 {
265 property: 'name',
52428d60
TL
266 direction: 'ASC',
267 },
268 ],
4ae75df3
DC
269 });
270
52428d60 271 let properties;
4ae75df3
DC
272 if (rtype === 'array' && retinf.items.properties) {
273 properties = retinf.items.properties;
274 }
275
276 if (rtype === 'object' && retinf.properties) {
277 properties = retinf.properties;
278 }
279
280 Ext.Object.each(properties, function(name, pdef) {
281 pdef.name = name;
282 rpstore.add(pdef);
283 });
284
285 rpstore.sort();
286
52428d60 287 let groupingFeature = Ext.create('Ext.grid.feature.Grouping', {
4ae75df3 288 enableGroupingMenu: false,
52428d60 289 groupHeaderTpl: '<tpl if="groupValue">Optional</tpl><tpl if="!groupValue">Obligatory</tpl>',
4ae75df3 290 });
52428d60 291 let returnhtml;
4ae75df3
DC
292 if (retinf.items) {
293 returnhtml = '<pre>items: ' + Ext.htmlEncode(JSON.stringify(retinf.items, null, 4)) + '</pre>';
294 }
295
296 if (retinf.properties) {
297 returnhtml = returnhtml || '';
298 returnhtml += '<pre>properties:' + Ext.htmlEncode(JSON.stringify(retinf.properties, null, 4)) + '</pre>';
299 }
300
52428d60 301 let rawSection = Ext.create('Ext.panel.Panel', {
4ae75df3
DC
302 bodyPadding: '0px 10px 10px 10px',
303 html: returnhtml,
52428d60 304 hidden: true,
4ae75df3
DC
305 });
306
307 sections.push({
308 xtype: 'gridpanel',
309 title: 'Returns: ' + rtype,
310 features: [groupingFeature],
311 store: rpstore,
312 viewConfig: {
313 trackOver: false,
52428d60 314 stripeRows: true,
4ae75df3 315 },
ac4fa7fe
TL
316 columns: [
317 {
318 header: 'Name',
319 dataIndex: 'name',
320 flex: 1,
52428d60 321 },
ac4fa7fe
TL
322 {
323 header: 'Type',
324 dataIndex: 'type',
325 renderer: render_type,
326 flex: 1,
327 },
328 {
329 header: 'Default',
330 dataIndex: 'default',
331 flex: 1,
332 },
333 {
334 header: 'Format',
335 dataIndex: 'type',
336 renderer: render_format,
337 flex: 2,
338 },
339 {
340 header: 'Description',
341 dataIndex: 'description',
342 renderer: render_description,
343 flex: 6,
344 },
345 ],
346 bbar: [
347 {
348 xtype: 'button',
349 text: 'Show RAW',
350 handler: function(btn) {
351 rawSection.setVisible(!rawSection.isVisible());
352 btn.setText(rawSection.isVisible() ? 'Hide RAW' : 'Show RAW');
353 },
354 },
355 ],
356 });
4ae75df3 357
ac4fa7fe 358 sections.push(rawSection);
4ae75df3
DC
359 }
360
361 if (!data.path.match(/\/_upgrade_/)) {
52428d60 362 let permhtml = '';
4ae75df3
DC
363
364 if (!info.permissions) {
365 permhtml = "Root only.";
366 } else {
367 if (info.permissions.description) {
368 permhtml += "<div style='white-space:pre-wrap;padding-bottom:10px;'>" +
369 Ext.htmlEncode(info.permissions.description) + "</div>";
370 }
371 permhtml += permission_text(info.permissions);
372 }
373
374 if (info.allowtoken !== undefined && !info.allowtoken) {
52428d60 375 permhtml += "<br />This API endpoint is not available for API tokens.";
4ae75df3
DC
376 }
377
378 sections.push({
379 title: 'Required permissions',
380 bodyPadding: 10,
52428d60 381 html: permhtml,
4ae75df3
DC
382 });
383 }
384
385 items.push({
386 title: method,
387 autoScroll: true,
388 defaults: {
52428d60 389 border: false,
4ae75df3 390 },
52428d60 391 items: sections,
4ae75df3
DC
392 });
393 }
394 });
395
52428d60
TL
396 let ct = Ext.getCmp('docview');
397 ct.setTitle("Path: " + real_path(data.path));
4ae75df3
DC
398 ct.removeAll(true);
399 ct.add(items);
400 ct.setActiveTab(0);
401 };
402
403 Ext.define('Ext.form.SearchField', {
404 extend: 'Ext.form.field.Text',
405 alias: 'widget.searchfield',
406
407 emptyText: 'Search...',
408
409 flex: 1,
410
411 inputType: 'search',
412 listeners: {
52428d60
TL
413 'change': function() {
414 let value = this.getValue();
4ae75df3
DC
415 if (!Ext.isEmpty(value)) {
416 store.filter({
417 property: 'path',
418 value: value,
52428d60 419 anyMatch: true,
4ae75df3
DC
420 });
421 } else {
422 store.clearFilter();
423 }
52428d60
TL
424 },
425 },
4ae75df3
DC
426 });
427
ac4fa7fe 428 let treePanel = Ext.create('Ext.tree.Panel', {
4ae75df3
DC
429 title: 'Resource Tree',
430 tbar: [
431 {
432 xtype: 'searchfield',
52428d60 433 },
4ae75df3
DC
434 ],
435 tools: [
436 {
437 type: 'expand',
438 tooltip: 'Expand all',
439 tooltipType: 'title',
ac4fa7fe 440 callback: tree => tree.expandAll(),
4ae75df3
DC
441 },
442 {
443 type: 'collapse',
444 tooltip: 'Collapse all',
445 tooltipType: 'title',
ac4fa7fe 446 callback: tree => tree.collapseAll(),
4ae75df3
DC
447 },
448 ],
449 store: store,
450 width: 200,
451 region: 'west',
452 split: true,
453 margins: '5 0 5 5',
454 rootVisible: false,
455 listeners: {
456 selectionchange: function(v, selections) {
52428d60
TL
457 if (!selections[0]) {return;}
458 let rec = selections[0];
4ae75df3
DC
459 render_docu(rec.data);
460 location.hash = '#' + rec.data.path;
52428d60
TL
461 },
462 },
4ae75df3
DC
463 });
464
465 Ext.create('Ext.container.Viewport', {
466 layout: 'border',
467 renderTo: Ext.getBody(),
468 items: [
ac4fa7fe 469 treePanel,
4ae75df3
DC
470 {
471 xtype: 'tabpanel',
472 title: 'Documentation',
473 id: 'docview',
474 region: 'center',
475 margins: '5 5 5 0',
476 layout: 'fit',
52428d60
TL
477 items: [],
478 },
479 ],
4ae75df3
DC
480 });
481
52428d60
TL
482 let deepLink = function() {
483 let path = window.location.hash.substring(1).replace(/\/\s*$/, '');
484 let endpoint = store.findNode('path', path);
4ae75df3
DC
485
486 if (endpoint) {
ac4fa7fe
TL
487 treePanel.getSelectionModel().select(endpoint);
488 treePanel.expandPath(endpoint.getPath());
4ae75df3
DC
489 render_docu(endpoint.data);
490 }
52428d60 491 };
4ae75df3
DC
492 window.onhashchange = deepLink;
493
494 deepLink();
4ae75df3 495});