]> git.proxmox.com Git - proxmox-widget-toolkit.git/blob - src/node/APTRepositories.js
subscription/summary/backup: stop setting the background color
[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: (record) => {
118 switch (record.data.status) {
119 case 'warning': return 'proxmox-warning-row';
120 case 'critical': return 'proxmox-invalid-row';
121 default: return '';
122 }
123 },
124 },
125
126 hideHeaders: true,
127
128 columns: [
129 {
130 dataIndex: 'status',
131 renderer: (value) => `<i class="fa fa-fw ${Proxmox.Utils.get_health_icon(value, true)}"></i>`,
132 width: 50,
133 },
134 {
135 dataIndex: 'message',
136 flex: 1,
137 },
138 ],
139 });
140
141 Ext.define('Proxmox.node.APTRepositoriesGrid', {
142 extend: 'Ext.grid.GridPanel',
143 xtype: 'proxmoxNodeAPTRepositoriesGrid',
144 mixins: ['Proxmox.Mixin.CBind'],
145
146 title: gettext('APT Repositories'),
147
148 cls: 'proxmox-apt-repos', // to allow applying styling to general components with local effect
149
150 border: false,
151
152 tbar: [
153 {
154 text: gettext('Reload'),
155 iconCls: 'fa fa-refresh',
156 handler: function() {
157 let me = this;
158 me.up('proxmoxNodeAPTRepositories').reload();
159 },
160 },
161 {
162 text: gettext('Add'),
163 name: 'addRepo',
164 disabled: true,
165 repoInfo: undefined,
166 cbind: {
167 onlineHelp: '{onlineHelp}',
168 },
169 handler: function(button, event, record) {
170 Proxmox.Utils.checked_command(() => {
171 let me = this;
172 let panel = me.up('proxmoxNodeAPTRepositories');
173
174 let extraParams = {};
175 if (panel.digest !== undefined) {
176 extraParams.digest = panel.digest;
177 }
178
179 Ext.create('Proxmox.window.APTRepositoryAdd', {
180 repoInfo: me.repoInfo,
181 url: `/api2/extjs/nodes/${panel.nodename}/apt/repositories`,
182 method: 'PUT',
183 extraRequestParams: extraParams,
184 onlineHelp: me.onlineHelp,
185 listeners: {
186 destroy: function() {
187 panel.reload();
188 },
189 },
190 }).show();
191 });
192 },
193 },
194 '-',
195 {
196 xtype: 'proxmoxAltTextButton',
197 defaultText: gettext('Enable'),
198 altText: gettext('Disable'),
199 name: 'repoEnable',
200 disabled: true,
201 bind: {
202 text: '{enableButtonText}',
203 },
204 handler: function(button, event, record) {
205 let me = this;
206 let panel = me.up('proxmoxNodeAPTRepositories');
207
208 let params = {
209 path: record.data.Path,
210 index: record.data.Index,
211 enabled: record.data.Enabled ? 0 : 1, // invert
212 };
213
214 if (panel.digest !== undefined) {
215 params.digest = panel.digest;
216 }
217
218 Proxmox.Utils.API2Request({
219 url: `/nodes/${panel.nodename}/apt/repositories`,
220 method: 'POST',
221 params: params,
222 failure: function(response, opts) {
223 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
224 panel.reload();
225 },
226 success: function(response, opts) {
227 panel.reload();
228 },
229 });
230 },
231 },
232 ],
233
234 sortableColumns: false,
235 viewConfig: {
236 stripeRows: false,
237 getRowClass: (record, index) => record.get('Enabled') ? '' : 'proxmox-disabled-row',
238 },
239
240 columns: [
241 {
242 xtype: 'checkcolumn',
243 header: gettext('Enabled'),
244 dataIndex: 'Enabled',
245 listeners: {
246 beforecheckchange: () => false, // veto, we don't want to allow inline change - to subtle
247 },
248 width: 90,
249 },
250 {
251 header: gettext('Types'),
252 dataIndex: 'Types',
253 renderer: function(types, cell, record) {
254 return types.join(' ');
255 },
256 width: 100,
257 },
258 {
259 header: gettext('URIs'),
260 dataIndex: 'URIs',
261 renderer: function(uris, cell, record) {
262 return uris.join(' ');
263 },
264 width: 350,
265 },
266 {
267 header: gettext('Suites'),
268 dataIndex: 'Suites',
269 renderer: function(suites, metaData, record) {
270 let err = '';
271 if (record.data.warnings && record.data.warnings.length > 0) {
272 let txt = [gettext('Warning')];
273 record.data.warnings.forEach((warning) => {
274 if (warning.property === 'Suites') {
275 txt.push(warning.message);
276 }
277 });
278 metaData.tdAttr = `data-qtip="${Ext.htmlEncode(txt.join('<br>'))}"`;
279 if (record.data.Enabled) {
280 metaData.tdCls = 'proxmox-invalid-row';
281 err = '<i class="fa fa-fw critical fa-exclamation-circle"></i> ';
282 } else {
283 metaData.tdCls = 'proxmox-warning-row';
284 err = '<i class="fa fa-fw warning fa-exclamation-circle"></i> ';
285 }
286 }
287 return suites.join(' ') + err;
288 },
289 width: 130,
290 },
291 {
292 header: gettext('Components'),
293 dataIndex: 'Components',
294 renderer: function(components, metaData, record) {
295 if (components === undefined) {
296 return '';
297 }
298 let err = '';
299 if (components.length === 1) {
300 // FIXME: this should be a flag set to the actual repsotiories, i.e., a tristate
301 // like production-ready = <yes|no|other> (Option<bool>)
302 if (components[0].match(/\w+(-no-subscription|test)\s*$/i)) {
303 metaData.tdCls = 'proxmox-warning-row';
304 err = '<i class="fa fa-fw warning fa-exclamation-circle"></i> ';
305
306 let qtip = components[0].match(/no-subscription/)
307 ? gettext('The no-subscription repository is NOT production-ready')
308 : gettext('The test repository may contain unstable updates')
309 ;
310 metaData.tdAttr = `data-qtip="${Ext.htmlEncode(qtip)}"`;
311 }
312 }
313 return components.join(' ') + err;
314 },
315 width: 170,
316 },
317 {
318 header: gettext('Options'),
319 dataIndex: 'Options',
320 renderer: function(options, cell, record) {
321 if (!options) {
322 return '';
323 }
324
325 let filetype = record.data.FileType;
326 let text = '';
327
328 options.forEach(function(option) {
329 let key = option.Key;
330 if (filetype === 'list') {
331 let values = option.Values.join(',');
332 text += `${key}=${values} `;
333 } else if (filetype === 'sources') {
334 let values = option.Values.join(' ');
335 text += `${key}: ${values}<br>`;
336 } else {
337 throw "unknown file type";
338 }
339 });
340 return text;
341 },
342 flex: 1,
343 },
344 {
345 header: gettext('Origin'),
346 dataIndex: 'Origin',
347 width: 120,
348 renderer: (value, meta, rec) => {
349 if (typeof value !== 'string' || value.length === 0) {
350 value = gettext('Other');
351 }
352 let cls = 'fa fa-fw fa-question-circle-o';
353 if (value.match(/^\s*Proxmox\s*$/i)) {
354 cls = 'pmx-itype-icon pmx-itype-icon-proxmox-x';
355 } else if (value.match(/^\s*Debian\s*(:?Backports)?$/i)) {
356 cls = 'pmx-itype-icon pmx-itype-icon-debian-swirl';
357 }
358 return `<i class='${cls}'></i> ${value}`;
359 },
360 },
361 {
362 header: gettext('Comment'),
363 dataIndex: 'Comment',
364 flex: 2,
365 },
366 ],
367
368 features: [
369 {
370 ftype: 'grouping',
371 groupHeaderTpl: '{[ "File: " + values.name ]} ({rows.length} repositor{[values.rows.length > 1 ? "ies" : "y"]})',
372 enableGroupingMenu: false,
373 },
374 ],
375
376 store: {
377 model: 'apt-repolist',
378 groupField: 'Path',
379 sorters: [
380 {
381 property: 'Index',
382 direction: 'ASC',
383 },
384 ],
385 },
386
387 initComponent: function() {
388 let me = this;
389
390 if (!me.nodename) {
391 throw "no node name specified";
392 }
393
394 me.callParent();
395 },
396 });
397
398 Ext.define('Proxmox.node.APTRepositories', {
399 extend: 'Ext.panel.Panel',
400 xtype: 'proxmoxNodeAPTRepositories',
401 mixins: ['Proxmox.Mixin.CBind'],
402
403 digest: undefined,
404
405 onlineHelp: 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 let store = vm.get('errorstore');
428 store.removeAll();
429
430 let status = 'good'; // start with best, the helper below will downgrade if needed
431 let text = gettext('All OK, you have production-ready repositories configured!');
432
433 let addGood = message => store.add({ status: 'good', message });
434 let addWarn = (message, important) => {
435 if (status !== 'critical') {
436 status = 'warning';
437 text = important ? message : gettext('Warning');
438 }
439 store.add({ status: 'warning', message });
440 };
441 let addCritical = (message, important) => {
442 status = 'critical';
443 text = important ? message : gettext('Error');
444 store.add({ status: 'critical', message });
445 };
446
447 let errors = vm.get('errors');
448 errors.forEach(error => addCritical(`${error.path} - ${error.error}`));
449
450 let activeSubscription = vm.get('subscriptionActive');
451 let enterprise = vm.get('enterpriseRepo');
452 let nosubscription = vm.get('noSubscriptionRepo');
453 let test = vm.get('testRepo');
454 let wrongSuites = vm.get('suitesWarning');
455
456 if (!enterprise && !nosubscription && !test) {
457 addCritical(
458 Ext.String.format(gettext('No {0} repository is enabled, you do not get any updates!'), vm.get('product')),
459 );
460 } else if (errors.length > 0) {
461 // nothing extra, just avoid that we show "get updates"
462 } else if (enterprise && !nosubscription && !test && activeSubscription) {
463 addGood(Ext.String.format(gettext('You get supported updates for {0}'), vm.get('product')));
464 } else if (nosubscription || test) {
465 addGood(Ext.String.format(gettext('You get updates for {0}'), vm.get('product')));
466 }
467
468 if (wrongSuites) {
469 addWarn(gettext('Some suites are misconfigured'));
470 }
471
472 if (!activeSubscription && enterprise) {
473 addWarn(gettext('The enterprise repository is enabled, but there is no active subscription!'));
474 }
475
476 if (nosubscription) {
477 addWarn(gettext('The no-subscription repository is not recommended for production use!'));
478 }
479
480 if (test) {
481 addWarn(gettext('The test repository may pull in unstable updates and is not recommended for production use!'));
482 }
483
484 if (errors.length > 0) {
485 text = gettext('Fatal parsing error for at least one repository');
486 }
487
488 let iconCls = Proxmox.Utils.get_health_icon(status, true);
489
490 vm.set('state', {
491 iconCls,
492 text,
493 });
494 },
495 },
496
497 viewModel: {
498 data: {
499 product: 'Proxmox VE', // default
500 errors: [],
501 suitesWarning: false,
502 subscriptionActive: '',
503 noSubscriptionRepo: '',
504 enterpriseRepo: '',
505 testRepo: '',
506 selectionenabled: false,
507 state: {},
508 },
509 formulas: {
510 enableButtonText: (get) => get('selectionenabled')
511 ? gettext('Disable') : gettext('Enable'),
512 },
513 stores: {
514 errorstore: {
515 fields: ['status', 'message'],
516 },
517 },
518 },
519
520 scrollable: true,
521 layout: {
522 type: 'vbox',
523 align: 'stretch',
524 },
525
526 items: [
527 {
528 xtype: 'panel',
529 border: false,
530 layout: {
531 type: 'hbox',
532 align: 'stretch',
533 },
534 height: 200,
535 title: gettext('Status'),
536 items: [
537 {
538 xtype: 'box',
539 flex: 2,
540 margin: 10,
541 data: {
542 iconCls: Proxmox.Utils.get_health_icon(undefined, true),
543 text: '',
544 },
545 bind: {
546 data: '{state}',
547 },
548 tpl: [
549 '<center class="centered-flex-column" style="font-size:15px;line-height: 25px;">',
550 '<i class="fa fa-4x {iconCls}"></i>',
551 '{text}',
552 '</center>',
553 ],
554 },
555 {
556 xtype: 'proxmoxNodeAPTRepositoriesErrors',
557 name: 'repositoriesErrors',
558 flex: 7,
559 margin: 10,
560 bind: {
561 store: '{errorstore}',
562 },
563 },
564 ],
565 },
566 {
567 xtype: 'proxmoxNodeAPTRepositoriesGrid',
568 name: 'repositoriesGrid',
569 flex: 1,
570 cbind: {
571 nodename: '{nodename}',
572 onlineHelp: '{onlineHelp}',
573 },
574 majorUpgradeAllowed: false, // TODO get release information from an API call?
575 listeners: {
576 selectionchange: 'selectionChange',
577 },
578 },
579 ],
580
581 check_subscription: function() {
582 let me = this;
583 let vm = me.getViewModel();
584
585 Proxmox.Utils.API2Request({
586 url: `/nodes/${me.nodename}/subscription`,
587 method: 'GET',
588 failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
589 success: function(response, opts) {
590 const res = response.result;
591 const subscription = !(!res || !res.data || res.data.status.toLowerCase() !== 'active');
592 vm.set('subscriptionActive', subscription);
593 me.getController().updateState();
594 },
595 });
596 },
597
598 updateStandardRepos: function(standardRepos) {
599 let me = this;
600 let vm = me.getViewModel();
601
602 let addButton = me.down('button[name=addRepo]');
603
604 addButton.repoInfo = [];
605 for (const standardRepo of standardRepos) {
606 const handle = standardRepo.handle;
607 const status = standardRepo.status;
608
609 if (handle === "enterprise") {
610 vm.set('enterpriseRepo', status);
611 } else if (handle === "no-subscription") {
612 vm.set('noSubscriptionRepo', status);
613 } else if (handle === 'test') {
614 vm.set('testRepo', status);
615 }
616 me.getController().updateState();
617
618 addButton.repoInfo.push(standardRepo);
619 addButton.digest = me.digest;
620 }
621
622 addButton.setDisabled(false);
623 },
624
625 reload: function() {
626 let me = this;
627 let vm = me.getViewModel();
628 let repoGrid = me.down('proxmoxNodeAPTRepositoriesGrid');
629
630 me.store.load(function(records, operation, success) {
631 let gridData = [];
632 let errors = [];
633 let digest;
634 let suitesWarning = false;
635
636 if (success && records.length > 0) {
637 let data = records[0].data;
638 let files = data.files;
639 errors = data.errors;
640 digest = data.digest;
641
642 let infos = {};
643 for (const info of data.infos) {
644 let path = info.path;
645 let idx = info.index;
646
647 if (!infos[path]) {
648 infos[path] = {};
649 }
650 if (!infos[path][idx]) {
651 infos[path][idx] = {
652 origin: '',
653 warnings: [],
654 };
655 }
656
657 if (info.kind === 'origin') {
658 infos[path][idx].origin = info.message;
659 } else if (info.kind === 'warning' ||
660 (info.kind === 'ignore-pre-upgrade-warning' && !repoGrid.majorUpgradeAllowed)
661 ) {
662 infos[path][idx].warnings.push(info);
663 } else {
664 throw 'unknown info';
665 }
666 }
667
668
669 files.forEach(function(file) {
670 for (let n = 0; n < file.repositories.length; n++) {
671 let repo = file.repositories[n];
672 repo.Path = file.path;
673 repo.Index = n;
674 if (infos[file.path] && infos[file.path][n]) {
675 repo.Origin = infos[file.path][n].origin || Proxmox.Utils.UnknownText;
676 repo.warnings = infos[file.path][n].warnings || [];
677
678 if (repo.Enabled && repo.warnings.some(w => w.property === 'Suites')) {
679 suitesWarning = true;
680 }
681 }
682 gridData.push(repo);
683 }
684 });
685
686 repoGrid.store.loadData(gridData);
687
688 me.updateStandardRepos(data['standard-repos']);
689 }
690
691 me.digest = digest;
692
693 vm.set('errors', errors);
694 vm.set('suitesWarning', suitesWarning);
695 me.getController().updateState();
696 });
697
698 me.check_subscription();
699 },
700
701 listeners: {
702 activate: function() {
703 let me = this;
704 me.reload();
705 },
706 },
707
708 initComponent: function() {
709 let me = this;
710
711 if (!me.nodename) {
712 throw "no node name specified";
713 }
714
715 let store = Ext.create('Ext.data.Store', {
716 proxy: {
717 type: 'proxmox',
718 url: `/api2/json/nodes/${me.nodename}/apt/repositories`,
719 },
720 });
721
722 Ext.apply(me, { store: store });
723
724 Proxmox.Utils.monStoreErrors(me, me.store, true);
725
726 me.callParent();
727
728 me.getViewModel().set('product', me.product);
729 },
730 });