]> git.proxmox.com Git - proxmox-widget-toolkit.git/blob - src/node/APTRepositories.js
5c832264cc7cfb67901c79905b4eb5ae24b793b2
[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
27 initComponent: function() {
28 let me = this;
29
30 if (!me.repoInfo || me.repoInfo.length === 0) {
31 throw "repository information not initialized";
32 }
33
34 let description = Ext.create('Ext.form.field.Display', {
35 fieldLabel: gettext('Description'),
36 name: 'description',
37 });
38
39 let status = Ext.create('Ext.form.field.Display', {
40 fieldLabel: gettext('Status'),
41 name: 'status',
42 renderer: function(value) {
43 let statusText = gettext('Not yet configured');
44 if (value !== '') {
45 statusText = Ext.String.format(
46 '{0}: {1}',
47 gettext('Configured'),
48 value ? gettext('enabled') : gettext('disabled'),
49 );
50 }
51
52 return statusText;
53 },
54 });
55
56 let repoSelector = Ext.create('Proxmox.form.KVComboBox', {
57 fieldLabel: gettext('Repository'),
58 xtype: 'proxmoxKVComboBox',
59 name: 'handle',
60 allowBlank: false,
61 comboItems: me.repoInfo.map(info => [info.handle, info.name]),
62 isValid: function() {
63 const handle = this.value;
64
65 if (!handle) {
66 return false;
67 }
68
69 const info = me.repoInfo.find(elem => elem.handle === handle);
70
71 if (!info) {
72 return false;
73 }
74
75 // not yet configured
76 return info.status === undefined || info.status === null;
77 },
78 listeners: {
79 change: function(f, value) {
80 const info = me.repoInfo.find(elem => elem.handle === value);
81 description.setValue(info.description);
82 status.setValue(info.status);
83 },
84 },
85 });
86
87 repoSelector.setValue(me.repoInfo[0].handle);
88
89 let items = [
90 repoSelector,
91 description,
92 status,
93 ];
94
95 Ext.apply(me, {
96 items: items,
97 repoSelector: repoSelector,
98 });
99
100 me.callParent();
101 },
102 });
103
104 Ext.define('Proxmox.node.APTRepositoriesErrors', {
105 extend: 'Ext.grid.GridPanel',
106
107 xtype: 'proxmoxNodeAPTRepositoriesErrors',
108
109 title: gettext('Errors'),
110
111 store: {},
112
113 border: false,
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/json/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 handler: function(button, event, record) {
192 let me = this;
193 let panel = me.up('proxmoxNodeAPTRepositories');
194
195 let params = {
196 path: record.data.Path,
197 index: record.data.Index,
198 enabled: record.data.Enabled ? 0 : 1, // invert
199 };
200
201 if (panel.digest !== undefined) {
202 params.digest = panel.digest;
203 }
204
205 Proxmox.Utils.API2Request({
206 url: `/nodes/${panel.nodename}/apt/repositories`,
207 method: 'POST',
208 params: params,
209 failure: function(response, opts) {
210 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
211 panel.reload();
212 },
213 success: function(response, opts) {
214 panel.reload();
215 },
216 });
217 },
218 listeners: {
219 render: function(btn) {
220 // HACK: calculate the max button width on first render to avoid toolbar glitches
221 let defSize = btn.getSize().width;
222
223 btn.setText(btn.altText);
224 let altSize = btn.getSize().width;
225
226 btn.setText(btn.defaultText);
227 btn.setSize({ width: altSize > defSize ? altSize : defSize });
228 },
229 },
230 },
231 ],
232
233 sortableColumns: false,
234
235 columns: [
236 {
237 xtype: 'checkcolumn',
238 header: gettext('Enabled'),
239 dataIndex: 'Enabled',
240 listeners: {
241 beforecheckchange: () => false, // veto, we don't want to allow inline change - to subtle
242 },
243 width: 90,
244 },
245 {
246 header: gettext('Types'),
247 dataIndex: 'Types',
248 renderer: function(types, cell, record) {
249 return types.join(' ');
250 },
251 width: 100,
252 },
253 {
254 header: gettext('URIs'),
255 dataIndex: 'URIs',
256 renderer: function(uris, cell, record) {
257 return uris.join(' ');
258 },
259 width: 350,
260 },
261 {
262 header: gettext('Suites'),
263 dataIndex: 'Suites',
264 renderer: function(suites, cell, record) {
265 return suites.join(' ');
266 },
267 width: 130,
268 },
269 {
270 header: gettext('Components'),
271 dataIndex: 'Components',
272 renderer: function(components, cell, record) {
273 return components.join(' ');
274 },
275 width: 170,
276 },
277 {
278 header: gettext('Options'),
279 dataIndex: 'Options',
280 renderer: function(options, cell, record) {
281 if (!options) {
282 return '';
283 }
284
285 let filetype = record.data.FileType;
286 let text = '';
287
288 options.forEach(function(option) {
289 let key = option.Key;
290 if (filetype === 'list') {
291 let values = option.Values.join(',');
292 text += `${key}=${values} `;
293 } else if (filetype === 'sources') {
294 let values = option.Values.join(' ');
295 text += `${key}: ${values}<br>`;
296 } else {
297 throw "unkown file type";
298 }
299 });
300 return text;
301 },
302 flex: 1,
303 },
304 {
305 header: gettext('Origin'),
306 dataIndex: 'Origin',
307 width: 120,
308 renderer: (value, meta, rec) => {
309 let cls = 'fa fa-fw fa-question-circle-o';
310 if (value.match(/^\s*Proxmox\s*$/i)) {
311 cls = 'pmx-itype-icon pmx-itype-icon-proxmox-x';
312 } else if (value.match(/^\s*Debian\s*$/i)) {
313 cls = 'pmx-itype-icon pmx-itype-icon-debian-swirl';
314 }
315 return `<i class='${cls}'></i> ${value}`;
316 },
317 },
318 {
319 header: gettext('Comment'),
320 dataIndex: 'Comment',
321 flex: 2,
322 },
323 ],
324
325 addAdditionalInfos: function(gridData, infos) {
326 let me = this;
327
328 let warnings = {};
329 let origins = {};
330
331 let addLine = function(obj, key, line) {
332 if (obj[key]) {
333 obj[key] += "\n";
334 obj[key] += line;
335 } else {
336 obj[key] = line;
337 }
338 };
339
340 for (const info of infos) {
341 const key = `${info.path}:${info.index}`;
342 if (info.kind === 'warning' ||
343 (info.kind === 'ignore-pre-upgrade-warning' && !me.majorUpgradeAllowed)
344 ) {
345 addLine(warnings, key, gettext('Warning') + ": " + info.message);
346 } else if (info.kind === 'origin') {
347 origins[key] = info.message;
348 }
349 }
350
351 gridData.forEach(function(record) {
352 const key = `${record.Path}:${record.Index}`;
353 record.Origin = origins[key];
354 });
355
356 me.rowBodyFeature.getAdditionalData = function(innerData, rowIndex, record, orig) {
357 let headerCt = this.view.headerCt;
358 let colspan = headerCt.getColumnCount();
359
360 const key = `${innerData.Path}:${innerData.Index}`;
361 const warning_text = warnings[key];
362
363 return {
364 rowBody: '<div style="color: red; white-space: pre-line">' +
365 Ext.String.htmlEncode(warning_text) + '</div>',
366 rowBodyCls: warning_text ? '' : Ext.baseCSSPrefix + 'grid-row-body-hidden',
367 rowBodyColspan: colspan,
368 };
369 };
370 },
371
372 initComponent: function() {
373 let me = this;
374
375 if (!me.nodename) {
376 throw "no node name specified";
377 }
378
379 let store = Ext.create('Ext.data.Store', {
380 model: 'apt-repolist',
381 groupField: 'Path',
382 sorters: [
383 {
384 property: 'Index',
385 direction: 'ASC',
386 },
387 ],
388 });
389
390 let rowBodyFeature = Ext.create('Ext.grid.feature.RowBody', {});
391
392 let groupingFeature = Ext.create('Ext.grid.feature.Grouping', {
393 groupHeaderTpl: '{[ "File: " + values.name ]} ({rows.length} ' +
394 'repositor{[values.rows.length > 1 ? "ies" : "y"]})',
395 enableGroupingMenu: false,
396 });
397
398 let sm = Ext.create('Ext.selection.RowModel', {});
399
400 Ext.apply(me, {
401 store: store,
402 selModel: sm,
403 rowBodyFeature: rowBodyFeature,
404 features: [groupingFeature, rowBodyFeature],
405 });
406
407 me.callParent();
408 },
409
410 listeners: {
411 selectionchange: function() {
412 let me = this;
413
414 if (me.onSelectionChange) {
415 let sm = me.getSelectionModel();
416 let rec = sm.getSelection()[0];
417
418 me.onSelectionChange(rec, sm);
419 }
420 },
421 },
422 });
423
424 Ext.define('Proxmox.node.APTRepositories', {
425 extend: 'Ext.panel.Panel',
426 xtype: 'proxmoxNodeAPTRepositories',
427 mixins: ['Proxmox.Mixin.CBind'],
428
429 digest: undefined,
430
431 product: 'Proxmox VE', // default
432
433 viewModel: {
434 data: {
435 product: 'Proxmox VE', // default
436 errorCount: 0,
437 subscriptionActive: '',
438 noSubscriptionRepo: '',
439 enterpriseRepo: '',
440 },
441 formulas: {
442 noErrors: (get) => get('errorCount') === 0,
443 mainWarning: function(get) {
444 // Not yet initialized
445 if (get('subscriptionActive') === '' ||
446 get('enterpriseRepo') === '') {
447 return '';
448 }
449
450 let icon = `<i class='fa fa-fw fa-exclamation-triangle critical'></i>`;
451 let fmt = (msg) => `<div class="black">${icon}${gettext('Warning')}: ${msg}</div>`;
452
453 if (!get('subscriptionActive') && get('enterpriseRepo')) {
454 return fmt(gettext('The enterprise repository is enabled, but there is no active subscription!'));
455 }
456
457 if (get('noSubscriptionRepo')) {
458 return fmt(gettext('The no-subscription repository is not recommended for production use!'));
459 }
460
461 if (!get('enterpriseRepo') && !get('noSubscriptionRepo')) {
462 let msg = Ext.String.format(gettext('No {0} repository is enabled!'), get('product'));
463 return fmt(msg);
464 }
465
466 return '';
467 },
468 },
469 },
470
471 scrollable: true,
472 layout: {
473 type: 'vbox',
474 align: 'stretch',
475 },
476
477 items: [
478 {
479 xtype: 'header',
480 baseCls: 'x-panel-header',
481 bind: {
482 hidden: '{!mainWarning}',
483 title: '{mainWarning}',
484 },
485 },
486 {
487 xtype: 'box',
488 bind: {
489 hidden: '{!mainWarning}',
490 },
491 height: 5,
492 },
493 {
494 xtype: 'proxmoxNodeAPTRepositoriesErrors',
495 name: 'repositoriesErrors',
496 hidden: true,
497 padding: '0 0 5 0',
498 bind: {
499 hidden: '{noErrors}',
500 },
501 },
502 {
503 xtype: 'proxmoxNodeAPTRepositoriesGrid',
504 name: 'repositoriesGrid',
505 cbind: {
506 nodename: '{nodename}',
507 },
508 majorUpgradeAllowed: false, // TODO get release information from an API call?
509 onSelectionChange: function(rec, sm) {
510 let me = this;
511 if (rec) {
512 let btn = me.up('proxmoxNodeAPTRepositories').down('#repoEnableButton');
513 btn.setText(rec.get('Enabled') ? gettext('Disable') : gettext('Enable'));
514 }
515 },
516 },
517 ],
518
519 check_subscription: function() {
520 let me = this;
521 let vm = me.getViewModel();
522
523 Proxmox.Utils.API2Request({
524 url: `/nodes/${me.nodename}/subscription`,
525 method: 'GET',
526 failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
527 success: function(response, opts) {
528 const res = response.result;
529 const subscription = !(!res || !res.data || res.data.status.toLowerCase() !== 'active');
530 vm.set('subscriptionActive', subscription);
531 },
532 });
533 },
534
535 updateStandardRepos: function(standardRepos) {
536 let me = this;
537 let vm = me.getViewModel();
538
539 let addButton = me.down('#addButton');
540 addButton.repoInfo = [];
541
542 for (const standardRepo of standardRepos) {
543 const handle = standardRepo.handle;
544 const status = standardRepo.status;
545
546 if (handle === "enterprise") {
547 vm.set('enterpriseRepo', status);
548 } else if (handle === "no-subscription") {
549 vm.set('noSubscriptionRepo', status);
550 }
551
552 addButton.repoInfo.push(standardRepo);
553 addButton.digest = me.digest;
554 }
555
556 addButton.setDisabled(false);
557 },
558
559 reload: function() {
560 let me = this;
561 let vm = me.getViewModel();
562 let repoGrid = me.down('proxmoxNodeAPTRepositoriesGrid');
563 let errorGrid = me.down('proxmoxNodeAPTRepositoriesErrors');
564
565 me.store.load(function(records, operation, success) {
566 let gridData = [];
567 let errors = [];
568 let digest;
569
570 if (success && records.length > 0) {
571 let data = records[0].data;
572 let files = data.files;
573 errors = data.errors;
574 digest = data.digest;
575
576 files.forEach(function(file) {
577 for (let n = 0; n < file.repositories.length; n++) {
578 let repo = file.repositories[n];
579 repo.Path = file.path;
580 repo.Index = n;
581 gridData.push(repo);
582 }
583 });
584
585 repoGrid.addAdditionalInfos(gridData, data.infos);
586 repoGrid.store.loadData(gridData);
587
588 me.updateStandardRepos(data['standard-repos']);
589 }
590
591 me.digest = digest;
592
593 vm.set('errorCount', errors.length);
594 errorGrid.store.loadData(errors);
595 });
596
597 me.check_subscription();
598 },
599
600 listeners: {
601 activate: function() {
602 let me = this;
603 me.reload();
604 },
605 },
606
607 initComponent: function() {
608 let me = this;
609
610 if (!me.nodename) {
611 throw "no node name specified";
612 }
613
614 let store = Ext.create('Ext.data.Store', {
615 proxy: {
616 type: 'proxmox',
617 url: `/api2/json/nodes/${me.nodename}/apt/repositories`,
618 },
619 });
620
621 Ext.apply(me, { store: store });
622
623 Proxmox.Utils.monStoreErrors(me, me.store, true);
624
625 me.callParent();
626
627 me.getViewModel().set('product', me.product);
628 },
629 });