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