]> git.proxmox.com Git - proxmox-widget-toolkit.git/blob - src/node/APTRepositories.js
node/APTRepositories: rework top status and error grid
[proxmox-widget-toolkit.git] / src / node / APTRepositories.js
1 Ext.define('apt-repolist', {
2 extend: 'Ext.data.Model',
3 fields: [
4 'Path',
5 'Index',
6 'Origin',
7 'FileType',
8 'Enabled',
9 'Comment',
10 'Types',
11 'URIs',
12 'Suites',
13 'Components',
14 'Options',
15 ],
16 });
17
18 Ext.define('Proxmox.window.APTRepositoryAdd', {
19 extend: 'Proxmox.window.Edit',
20 alias: 'widget.pmxAPTRepositoryAdd',
21
22 isCreate: true,
23 isAdd: true,
24
25 subject: gettext('Repository'),
26 width: 600,
27
28 initComponent: function() {
29 let me = this;
30
31 if (!me.repoInfo || me.repoInfo.length === 0) {
32 throw "repository information not initialized";
33 }
34
35 let description = Ext.create('Ext.form.field.Display', {
36 fieldLabel: gettext('Description'),
37 name: 'description',
38 });
39
40 let status = Ext.create('Ext.form.field.Display', {
41 fieldLabel: gettext('Status'),
42 name: 'status',
43 renderer: function(value) {
44 let statusText = gettext('Not yet configured');
45 if (value !== '') {
46 statusText = Ext.String.format(
47 '{0}: {1}',
48 gettext('Configured'),
49 value ? gettext('enabled') : gettext('disabled'),
50 );
51 }
52
53 return statusText;
54 },
55 });
56
57 let repoSelector = Ext.create('Proxmox.form.KVComboBox', {
58 fieldLabel: gettext('Repository'),
59 xtype: 'proxmoxKVComboBox',
60 name: 'handle',
61 allowBlank: false,
62 comboItems: me.repoInfo.map(info => [info.handle, info.name]),
63 validator: function(renderedValue) {
64 let handle = this.value;
65 // we cannot use this.callParent in instantiations
66 let valid = Proxmox.form.KVComboBox.prototype.validator.call(this, renderedValue);
67
68 if (!valid || !handle) {
69 return false;
70 }
71
72 const info = me.repoInfo.find(elem => elem.handle === handle);
73 if (!info) {
74 return false;
75 }
76
77 if (info.status) {
78 return Ext.String.format(gettext('{0} is already configured'), renderedValue);
79 }
80 return valid;
81 },
82 listeners: {
83 change: function(f, value) {
84 const info = me.repoInfo.find(elem => elem.handle === value);
85 description.setValue(info.description);
86 status.setValue(info.status);
87 },
88 },
89 });
90
91 repoSelector.setValue(me.repoInfo[0].handle);
92
93 Ext.apply(me, {
94 items: [
95 repoSelector,
96 description,
97 status,
98 ],
99 repoSelector: repoSelector,
100 });
101
102 me.callParent();
103 },
104 });
105
106 Ext.define('Proxmox.node.APTRepositoriesErrors', {
107 extend: 'Ext.grid.GridPanel',
108
109 xtype: 'proxmoxNodeAPTRepositoriesErrors',
110
111 store: {},
112
113 scrollable: true,
114
115 viewConfig: {
116 stripeRows: false,
117 getRowClass: () => 'proxmox-invalid-row',
118 },
119
120 columns: [
121 {
122 header: gettext('File'),
123 dataIndex: 'path',
124 renderer: value => `<i class='pve-grid-fa fa fa-fw fa-exclamation-triangle'></i>${value}`,
125 width: 350,
126 },
127 {
128 header: gettext('Error'),
129 dataIndex: 'error',
130 flex: 1,
131 },
132 ],
133 });
134
135 Ext.define('Proxmox.node.APTRepositoriesGrid', {
136 extend: 'Ext.grid.GridPanel',
137 xtype: 'proxmoxNodeAPTRepositoriesGrid',
138
139 title: gettext('APT Repositories'),
140
141 cls: 'proxmox-apt-repos', // to allow applying styling to general components with local effect
142
143 border: false,
144
145 tbar: [
146 {
147 text: gettext('Reload'),
148 iconCls: 'fa fa-refresh',
149 handler: function() {
150 let me = this;
151 me.up('proxmoxNodeAPTRepositories').reload();
152 },
153 },
154 {
155 text: gettext('Add'),
156 id: 'addButton',
157 disabled: true,
158 repoInfo: undefined,
159 handler: function(button, event, record) {
160 Proxmox.Utils.checked_command(() => {
161 let me = this;
162 let panel = me.up('proxmoxNodeAPTRepositories');
163
164 let extraParams = {};
165 if (panel.digest !== undefined) {
166 extraParams.digest = panel.digest;
167 }
168
169 Ext.create('Proxmox.window.APTRepositoryAdd', {
170 repoInfo: me.repoInfo,
171 url: `/api2/extjs/nodes/${panel.nodename}/apt/repositories`,
172 method: 'PUT',
173 extraRequestParams: extraParams,
174 listeners: {
175 destroy: function() {
176 panel.reload();
177 },
178 },
179 }).show();
180 });
181 },
182 },
183 '-',
184 {
185 xtype: 'proxmoxButton',
186 text: gettext('Enable'),
187 defaultText: gettext('Enable'),
188 altText: gettext('Disable'),
189 id: 'repoEnableButton',
190 disabled: true,
191 bind: {
192 text: '{enableButtonText}',
193 },
194 handler: function(button, event, record) {
195 let me = this;
196 let panel = me.up('proxmoxNodeAPTRepositories');
197
198 let params = {
199 path: record.data.Path,
200 index: record.data.Index,
201 enabled: record.data.Enabled ? 0 : 1, // invert
202 };
203
204 if (panel.digest !== undefined) {
205 params.digest = panel.digest;
206 }
207
208 Proxmox.Utils.API2Request({
209 url: `/nodes/${panel.nodename}/apt/repositories`,
210 method: 'POST',
211 params: params,
212 failure: function(response, opts) {
213 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
214 panel.reload();
215 },
216 success: function(response, opts) {
217 panel.reload();
218 },
219 });
220 },
221 listeners: {
222 render: function(btn) {
223 // HACK: calculate the max button width on first render to avoid toolbar glitches
224 let defSize = btn.getSize().width;
225
226 btn.setText(btn.altText);
227 let altSize = btn.getSize().width;
228
229 btn.setText(btn.defaultText);
230 btn.setSize({ width: altSize > defSize ? altSize : defSize });
231 },
232 },
233 },
234 ],
235
236 sortableColumns: false,
237 viewConfig: {
238 stripeRows: false,
239 getRowClass: (record, index) => record.get('Enabled') ? '' : 'proxmox-disabled-row',
240 },
241
242 columns: [
243 {
244 xtype: 'checkcolumn',
245 header: gettext('Enabled'),
246 dataIndex: 'Enabled',
247 listeners: {
248 beforecheckchange: () => false, // veto, we don't want to allow inline change - to subtle
249 },
250 width: 90,
251 },
252 {
253 header: gettext('Types'),
254 dataIndex: 'Types',
255 renderer: function(types, cell, record) {
256 return types.join(' ');
257 },
258 width: 100,
259 },
260 {
261 header: gettext('URIs'),
262 dataIndex: 'URIs',
263 renderer: function(uris, cell, record) {
264 return uris.join(' ');
265 },
266 width: 350,
267 },
268 {
269 header: gettext('Suites'),
270 dataIndex: 'Suites',
271 renderer: function(suites, metaData, record) {
272 let err = '';
273 if (record.data.warnings && record.data.warnings.length > 0) {
274 let txt = [gettext('Warning')];
275 record.data.warnings.forEach((warning) => {
276 if (warning.property === 'Suites') {
277 txt.push(warning.message);
278 }
279 });
280 metaData.tdAttr = `data-qtip="${Ext.htmlEncode(txt.join('<br>'))}"`;
281 if (record.data.Enabled) {
282 metaData.tdCls = 'proxmox-invalid-row';
283 err = '<i class="fa fa-fw critical fa-exclamation-circle"></i> ';
284 } else {
285 metaData.tdCls = 'proxmox-warning-row';
286 err = '<i class="fa fa-fw warning fa-exclamation-circle"></i> ';
287 }
288 }
289 return suites.join(' ') + err;
290 },
291 width: 130,
292 },
293 {
294 header: gettext('Components'),
295 dataIndex: 'Components',
296 renderer: function(components, metaData, record) {
297 let err = '';
298 if (components.length === 1) {
299 // FIXME: this should be a flag set to the actual repsotiories, i.e., a tristate
300 // like production-ready = <yes|no|other> (Option<bool>)
301 if (components[0].match(/\w+(-no-subscription|test)\s*$/i)) {
302 metaData.tdCls = 'proxmox-warning-row';
303 err = '<i class="fa fa-fw warning fa-exclamation-circle"></i> ';
304
305 let qtip = components[0].match(/no-subscription/)
306 ? gettext('The no-subscription repository is NOT production-ready')
307 : gettext('The test repository may contain unstable updates')
308 ;
309 metaData.tdAttr = `data-qtip="${Ext.htmlEncode(qtip)}"`;
310 }
311 }
312 return components.join(' ') + err;
313 },
314 width: 170,
315 },
316 {
317 header: gettext('Options'),
318 dataIndex: 'Options',
319 renderer: function(options, cell, record) {
320 if (!options) {
321 return '';
322 }
323
324 let filetype = record.data.FileType;
325 let text = '';
326
327 options.forEach(function(option) {
328 let key = option.Key;
329 if (filetype === 'list') {
330 let values = option.Values.join(',');
331 text += `${key}=${values} `;
332 } else if (filetype === 'sources') {
333 let values = option.Values.join(' ');
334 text += `${key}: ${values}<br>`;
335 } else {
336 throw "unkown file type";
337 }
338 });
339 return text;
340 },
341 flex: 1,
342 },
343 {
344 header: gettext('Origin'),
345 dataIndex: 'Origin',
346 width: 120,
347 renderer: (value, meta, rec) => {
348 if (typeof value !== 'string' || value.length === 0) {
349 value = gettext('Other');
350 }
351 let cls = 'fa fa-fw fa-question-circle-o';
352 if (value.match(/^\s*Proxmox\s*$/i)) {
353 cls = 'pmx-itype-icon pmx-itype-icon-proxmox-x';
354 } else if (value.match(/^\s*Debian\s*$/i)) {
355 cls = 'pmx-itype-icon pmx-itype-icon-debian-swirl';
356 }
357 return `<i class='${cls}'></i> ${value}`;
358 },
359 },
360 {
361 header: gettext('Comment'),
362 dataIndex: 'Comment',
363 flex: 2,
364 },
365 ],
366
367 initComponent: function() {
368 let me = this;
369
370 if (!me.nodename) {
371 throw "no node name specified";
372 }
373
374 let store = Ext.create('Ext.data.Store', {
375 model: 'apt-repolist',
376 groupField: 'Path',
377 sorters: [
378 {
379 property: 'Index',
380 direction: 'ASC',
381 },
382 ],
383 });
384
385 let groupingFeature = Ext.create('Ext.grid.feature.Grouping', {
386 groupHeaderTpl: '{[ "File: " + values.name ]} ({rows.length} ' +
387 'repositor{[values.rows.length > 1 ? "ies" : "y"]})',
388 enableGroupingMenu: false,
389 });
390
391 Ext.apply(me, {
392 store: store,
393 features: [groupingFeature],
394 });
395
396 me.callParent();
397 },
398 });
399
400 Ext.define('Proxmox.node.APTRepositories', {
401 extend: 'Ext.panel.Panel',
402 xtype: 'proxmoxNodeAPTRepositories',
403 mixins: ['Proxmox.Mixin.CBind'],
404
405 digest: undefined,
406
407 product: 'Proxmox VE', // default
408
409 controller: {
410 xclass: 'Ext.app.ViewController',
411
412 selectionChange: function(grid, selection) {
413 let me = this;
414 if (!selection || selection.length < 1) {
415 return;
416 }
417 let rec = selection[0];
418 let vm = me.getViewModel();
419 vm.set('selectionenabled', rec.get('Enabled'));
420 vm.notify();
421 },
422
423 updateState: function() {
424 let me = this;
425 let vm = me.getViewModel();
426
427 if (vm.get('errorCount') > 0) {
428 vm.set('state', {
429 iconCls: Proxmox.Utils.get_health_icon('critical', true),
430 text: gettext('Error parsing repositories'),
431 });
432 return;
433 }
434
435 let text = gettext('Repositories are configured in a recommended way');
436 let status = 'warning';
437
438 let activeSubscription = vm.get('subscriptionActive');
439 let enterprise = vm.get('enterpriseRepo');
440 let nosubscription = vm.get('noSubscriptionRepo');
441 let test = vm.get('testRepo');
442 let wrongSuites = vm.get('suitesWarning');
443
444 if (!activeSubscription && enterprise) {
445 text = gettext('The enterprise repository is enabled, but there is no active subscription!');
446 } else if (nosubscription) {
447 text = gettext('The no-subscription repository is not recommended for production use!');
448 } else if (test) {
449 text = gettext('The test repository is not recommended for production use!');
450 } else if (!enterprise && !nosubscription && !test) {
451 text = Ext.String.format(gettext('No {0} repository is enabled!'), vm.get('product'));
452 } else if (wrongSuites) {
453 text = gettext('Some Suites are misconfigured');
454 } else {
455 status = 'good';
456 }
457
458 let iconCls = Proxmox.Utils.get_health_icon(status, true);
459
460 vm.set('state', {
461 iconCls,
462 text,
463 });
464 },
465 },
466
467 viewModel: {
468 data: {
469 product: 'Proxmox VE', // default
470 errorCount: 0,
471 suitesWarning: false,
472 subscriptionActive: '',
473 noSubscriptionRepo: '',
474 enterpriseRepo: '',
475 testRepo: '',
476 selectionenabled: false,
477 state: {},
478 },
479 formulas: {
480 noErrors: (get) => get('errorCount') === 0,
481 enableButtonText: (get) => get('selectionenabled')
482 ? gettext('Disable') : gettext('Enable'),
483 },
484 },
485
486 scrollable: true,
487 layout: {
488 type: 'vbox',
489 align: 'stretch',
490 },
491
492 items: [
493 {
494 xtype: 'panel',
495 border: false,
496 layout: {
497 type: 'hbox',
498 align: 'stretch',
499 },
500 height: 150,
501 title: gettext('Status'),
502 items: [
503 {
504 xtype: 'box',
505 flex: 1,
506 margin: 10,
507 data: {
508 iconCls: Proxmox.Utils.get_health_icon(undefined, true),
509 text: '',
510 },
511 bind: {
512 data: '{state}',
513 },
514 tpl: [
515 '<center>',
516 '<i class="fa fa-4x {iconCls}"></i>',
517 '<br/><br/>',
518 '{text}',
519 '</center>',
520 ],
521 },
522 {
523 xtype: 'proxmoxNodeAPTRepositoriesErrors',
524 name: 'repositoriesErrors',
525 flex: 2,
526 hidden: true,
527 margin: 10,
528 bind: {
529 hidden: '{noErrors}',
530 },
531 },
532 ],
533 },
534 {
535 xtype: 'proxmoxNodeAPTRepositoriesGrid',
536 name: 'repositoriesGrid',
537 flex: 1,
538 cbind: {
539 nodename: '{nodename}',
540 },
541 majorUpgradeAllowed: false, // TODO get release information from an API call?
542 listeners: {
543 selectionchange: 'selectionChange',
544 },
545 },
546 ],
547
548 check_subscription: function() {
549 let me = this;
550 let vm = me.getViewModel();
551
552 Proxmox.Utils.API2Request({
553 url: `/nodes/${me.nodename}/subscription`,
554 method: 'GET',
555 failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
556 success: function(response, opts) {
557 const res = response.result;
558 const subscription = !(!res || !res.data || res.data.status.toLowerCase() !== 'active');
559 vm.set('subscriptionActive', subscription);
560 me.getController().updateState();
561 },
562 });
563 },
564
565 updateStandardRepos: function(standardRepos) {
566 let me = this;
567 let vm = me.getViewModel();
568
569 let addButton = me.down('#addButton');
570 addButton.repoInfo = [];
571
572 for (const standardRepo of standardRepos) {
573 const handle = standardRepo.handle;
574 const status = standardRepo.status;
575
576 if (handle === "enterprise") {
577 vm.set('enterpriseRepo', status);
578 } else if (handle === "no-subscription") {
579 vm.set('noSubscriptionRepo', status);
580 } else if (handle === 'test') {
581 vm.set('testRepo', status);
582 }
583 me.getController().updateState();
584
585 addButton.repoInfo.push(standardRepo);
586 addButton.digest = me.digest;
587 }
588
589 addButton.setDisabled(false);
590 },
591
592 reload: function() {
593 let me = this;
594 let vm = me.getViewModel();
595 let repoGrid = me.down('proxmoxNodeAPTRepositoriesGrid');
596 let errorGrid = me.down('proxmoxNodeAPTRepositoriesErrors');
597
598 me.store.load(function(records, operation, success) {
599 let gridData = [];
600 let errors = [];
601 let digest;
602 let suitesWarning = false;
603
604 if (success && records.length > 0) {
605 let data = records[0].data;
606 let files = data.files;
607 errors = data.errors;
608 digest = data.digest;
609
610 let infos = {};
611 for (const info of data.infos) {
612 let path = info.path;
613 let idx = info.index;
614
615 if (!infos[path]) {
616 infos[path] = {};
617 }
618 if (!infos[path][idx]) {
619 infos[path][idx] = {
620 origin: '',
621 warnings: [],
622 };
623 }
624
625 if (info.kind === 'origin') {
626 infos[path][idx].origin = info.message;
627 } else if (info.kind === 'warning' ||
628 (info.kind === 'ignore-pre-upgrade-warning' && !repoGrid.majorUpgradeAllowed)
629 ) {
630 infos[path][idx].warnings.push(info);
631 if (!suitesWarning && info.property === 'Suites') {
632 suitesWarning = true;
633 }
634 } else {
635 throw 'unknown info';
636 }
637 }
638
639
640 files.forEach(function(file) {
641 for (let n = 0; n < file.repositories.length; n++) {
642 let repo = file.repositories[n];
643 repo.Path = file.path;
644 repo.Index = n;
645 if (infos[file.path] && infos[file.path][n]) {
646 repo.Origin = infos[file.path][n].origin || Proxmox.Utils.UnknownText;
647 repo.warnings = infos[file.path][n].warnings || [];
648 }
649 gridData.push(repo);
650 }
651 });
652
653 repoGrid.store.loadData(gridData);
654
655 me.updateStandardRepos(data['standard-repos']);
656 }
657
658 me.digest = digest;
659
660 vm.set('errorCount', errors.length);
661 vm.set('suitesWarning', suitesWarning);
662 me.getController().updateState();
663
664 errorGrid.store.loadData(errors);
665 });
666
667 me.check_subscription();
668 },
669
670 listeners: {
671 activate: function() {
672 let me = this;
673 me.reload();
674 },
675 },
676
677 initComponent: function() {
678 let me = this;
679
680 if (!me.nodename) {
681 throw "no node name specified";
682 }
683
684 let store = Ext.create('Ext.data.Store', {
685 proxy: {
686 type: 'proxmox',
687 url: `/api2/json/nodes/${me.nodename}/apt/repositories`,
688 },
689 });
690
691 Ext.apply(me, { store: store });
692
693 Proxmox.Utils.monStoreErrors(me, me.store, true);
694
695 me.callParent();
696
697 me.getViewModel().set('product', me.product);
698 },
699 });