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