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