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