]> git.proxmox.com Git - proxmox-widget-toolkit.git/blob - src/node/APTRepositories.js
387682546a17c8d4935c0831690a2b0dc12f28f0
[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 title: gettext('Errors'),
112
113 store: {},
114
115 border: false,
116
117 viewConfig: {
118 stripeRows: false,
119 getRowClass: () => 'proxmox-invalid-row',
120 },
121
122 columns: [
123 {
124 header: gettext('File'),
125 dataIndex: 'path',
126 renderer: value => `<i class='pve-grid-fa fa fa-fw fa-exclamation-triangle'></i>${value}`,
127 width: 350,
128 },
129 {
130 header: gettext('Error'),
131 dataIndex: 'error',
132 flex: 1,
133 },
134 ],
135 });
136
137 Ext.define('Proxmox.node.APTRepositoriesGrid', {
138 extend: 'Ext.grid.GridPanel',
139 xtype: 'proxmoxNodeAPTRepositoriesGrid',
140
141 title: gettext('APT Repositories'),
142
143 cls: 'proxmox-apt-repos', // to allow applying styling to general components with local effect
144
145 border: false,
146
147 tbar: [
148 {
149 text: gettext('Reload'),
150 iconCls: 'fa fa-refresh',
151 handler: function() {
152 let me = this;
153 me.up('proxmoxNodeAPTRepositories').reload();
154 },
155 },
156 {
157 text: gettext('Add'),
158 id: 'addButton',
159 disabled: true,
160 repoInfo: undefined,
161 handler: function(button, event, record) {
162 Proxmox.Utils.checked_command(() => {
163 let me = this;
164 let panel = me.up('proxmoxNodeAPTRepositories');
165
166 let extraParams = {};
167 if (panel.digest !== undefined) {
168 extraParams.digest = panel.digest;
169 }
170
171 Ext.create('Proxmox.window.APTRepositoryAdd', {
172 repoInfo: me.repoInfo,
173 url: `/api2/extjs/nodes/${panel.nodename}/apt/repositories`,
174 method: 'PUT',
175 extraRequestParams: extraParams,
176 listeners: {
177 destroy: function() {
178 panel.reload();
179 },
180 },
181 }).show();
182 });
183 },
184 },
185 '-',
186 {
187 xtype: 'proxmoxButton',
188 text: gettext('Enable'),
189 defaultText: gettext('Enable'),
190 altText: gettext('Disable'),
191 id: 'repoEnableButton',
192 disabled: true,
193 bind: {
194 text: '{enableButtonText}',
195 },
196 handler: function(button, event, record) {
197 let me = this;
198 let panel = me.up('proxmoxNodeAPTRepositories');
199
200 let params = {
201 path: record.data.Path,
202 index: record.data.Index,
203 enabled: record.data.Enabled ? 0 : 1, // invert
204 };
205
206 if (panel.digest !== undefined) {
207 params.digest = panel.digest;
208 }
209
210 Proxmox.Utils.API2Request({
211 url: `/nodes/${panel.nodename}/apt/repositories`,
212 method: 'POST',
213 params: params,
214 failure: function(response, opts) {
215 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
216 panel.reload();
217 },
218 success: function(response, opts) {
219 panel.reload();
220 },
221 });
222 },
223 listeners: {
224 render: function(btn) {
225 // HACK: calculate the max button width on first render to avoid toolbar glitches
226 let defSize = btn.getSize().width;
227
228 btn.setText(btn.altText);
229 let altSize = btn.getSize().width;
230
231 btn.setText(btn.defaultText);
232 btn.setSize({ width: altSize > defSize ? altSize : defSize });
233 },
234 },
235 },
236 ],
237
238 sortableColumns: false,
239 viewConfig: {
240 stripeRows: false,
241 getRowClass: (record, index) => record.get('Enabled') ? '' : 'proxmox-disabled-row',
242 },
243
244 columns: [
245 {
246 xtype: 'checkcolumn',
247 header: gettext('Enabled'),
248 dataIndex: 'Enabled',
249 listeners: {
250 beforecheckchange: () => false, // veto, we don't want to allow inline change - to subtle
251 },
252 width: 90,
253 },
254 {
255 header: gettext('Types'),
256 dataIndex: 'Types',
257 renderer: function(types, cell, record) {
258 return types.join(' ');
259 },
260 width: 100,
261 },
262 {
263 header: gettext('URIs'),
264 dataIndex: 'URIs',
265 renderer: function(uris, cell, record) {
266 return uris.join(' ');
267 },
268 width: 350,
269 },
270 {
271 header: gettext('Suites'),
272 dataIndex: 'Suites',
273 renderer: function(suites, metaData, record) {
274 let err = '';
275 if (record.data.warnings && record.data.warnings.length > 0) {
276 let txt = [gettext('Warning')];
277 record.data.warnings.forEach((warning) => {
278 if (warning.property === 'Suites') {
279 txt.push(warning.message);
280 }
281 });
282 metaData.tdAttr = `data-qtip="${Ext.htmlEncode(txt.join('<br>'))}"`;
283 if (record.data.Enabled) {
284 metaData.tdCls = 'proxmox-invalid-row';
285 err = '<i class="fa fa-fw critical fa-exclamation-circle"></i> ';
286 } else {
287 metaData.tdCls = 'proxmox-warning-row';
288 err = '<i class="fa fa-fw warning fa-exclamation-circle"></i> ';
289 }
290 }
291 return suites.join(' ') + err;
292 },
293 width: 130,
294 },
295 {
296 header: gettext('Components'),
297 dataIndex: 'Components',
298 renderer: function(components, cell, record) {
299 return components.join(' ');
300 },
301 width: 170,
302 },
303 {
304 header: gettext('Options'),
305 dataIndex: 'Options',
306 renderer: function(options, cell, record) {
307 if (!options) {
308 return '';
309 }
310
311 let filetype = record.data.FileType;
312 let text = '';
313
314 options.forEach(function(option) {
315 let key = option.Key;
316 if (filetype === 'list') {
317 let values = option.Values.join(',');
318 text += `${key}=${values} `;
319 } else if (filetype === 'sources') {
320 let values = option.Values.join(' ');
321 text += `${key}: ${values}<br>`;
322 } else {
323 throw "unkown file type";
324 }
325 });
326 return text;
327 },
328 flex: 1,
329 },
330 {
331 header: gettext('Origin'),
332 dataIndex: 'Origin',
333 width: 120,
334 renderer: (value, meta, rec) => {
335 if (typeof value !== 'string' || value.length === 0) {
336 value = gettext('Other');
337 }
338 let cls = 'fa fa-fw fa-question-circle-o';
339 if (value.match(/^\s*Proxmox\s*$/i)) {
340 cls = 'pmx-itype-icon pmx-itype-icon-proxmox-x';
341 } else if (value.match(/^\s*Debian\s*$/i)) {
342 cls = 'pmx-itype-icon pmx-itype-icon-debian-swirl';
343 }
344 return `<i class='${cls}'></i> ${value}`;
345 },
346 },
347 {
348 header: gettext('Comment'),
349 dataIndex: 'Comment',
350 flex: 2,
351 },
352 ],
353
354 initComponent: function() {
355 let me = this;
356
357 if (!me.nodename) {
358 throw "no node name specified";
359 }
360
361 let store = Ext.create('Ext.data.Store', {
362 model: 'apt-repolist',
363 groupField: 'Path',
364 sorters: [
365 {
366 property: 'Index',
367 direction: 'ASC',
368 },
369 ],
370 });
371
372 let groupingFeature = Ext.create('Ext.grid.feature.Grouping', {
373 groupHeaderTpl: '{[ "File: " + values.name ]} ({rows.length} ' +
374 'repositor{[values.rows.length > 1 ? "ies" : "y"]})',
375 enableGroupingMenu: false,
376 });
377
378 Ext.apply(me, {
379 store: store,
380 features: [groupingFeature],
381 });
382
383 me.callParent();
384 },
385 });
386
387 Ext.define('Proxmox.node.APTRepositories', {
388 extend: 'Ext.panel.Panel',
389 xtype: 'proxmoxNodeAPTRepositories',
390 mixins: ['Proxmox.Mixin.CBind'],
391
392 digest: undefined,
393
394 product: 'Proxmox VE', // default
395
396 controller: {
397 xclass: 'Ext.app.ViewController',
398
399 selectionChange: function(grid, selection) {
400 let me = this;
401 if (!selection || selection.length < 1) {
402 return;
403 }
404 let rec = selection[0];
405 let vm = me.getViewModel();
406 vm.set('selectionenabled', rec.get('Enabled'));
407 },
408 },
409
410 viewModel: {
411 data: {
412 product: 'Proxmox VE', // default
413 errorCount: 0,
414 subscriptionActive: '',
415 noSubscriptionRepo: '',
416 enterpriseRepo: '',
417 selectionenabled: false,
418 },
419 formulas: {
420 noErrors: (get) => get('errorCount') === 0,
421 enableButtonText: (get) => get('selectionenabled')
422 ? gettext('Disable') : gettext('Enable'),
423 mainWarning: function(get) {
424 // Not yet initialized
425 if (get('subscriptionActive') === '' ||
426 get('enterpriseRepo') === '') {
427 return '';
428 }
429
430 let icon = `<i class='fa fa-fw fa-exclamation-triangle critical'></i>`;
431 let fmt = (msg) => `<div class="black">${icon}${gettext('Warning')}: ${msg}</div>`;
432
433 if (!get('subscriptionActive') && get('enterpriseRepo')) {
434 return fmt(gettext('The enterprise repository is enabled, but there is no active subscription!'));
435 }
436
437 if (get('noSubscriptionRepo')) {
438 return fmt(gettext('The no-subscription repository is not recommended for production use!'));
439 }
440
441 if (!get('enterpriseRepo') && !get('noSubscriptionRepo')) {
442 let msg = Ext.String.format(gettext('No {0} repository is enabled!'), get('product'));
443 return fmt(msg);
444 }
445
446 return '';
447 },
448 },
449 },
450
451 scrollable: true,
452 layout: {
453 type: 'vbox',
454 align: 'stretch',
455 },
456
457 items: [
458 {
459 xtype: 'header',
460 baseCls: 'x-panel-header',
461 bind: {
462 hidden: '{!mainWarning}',
463 title: '{mainWarning}',
464 },
465 },
466 {
467 xtype: 'box',
468 bind: {
469 hidden: '{!mainWarning}',
470 },
471 height: 5,
472 },
473 {
474 xtype: 'proxmoxNodeAPTRepositoriesErrors',
475 name: 'repositoriesErrors',
476 hidden: true,
477 padding: '0 0 5 0',
478 bind: {
479 hidden: '{noErrors}',
480 },
481 },
482 {
483 xtype: 'proxmoxNodeAPTRepositoriesGrid',
484 name: 'repositoriesGrid',
485 cbind: {
486 nodename: '{nodename}',
487 },
488 majorUpgradeAllowed: false, // TODO get release information from an API call?
489 listeners: {
490 selectionchange: 'selectionChange',
491 },
492 },
493 ],
494
495 check_subscription: function() {
496 let me = this;
497 let vm = me.getViewModel();
498
499 Proxmox.Utils.API2Request({
500 url: `/nodes/${me.nodename}/subscription`,
501 method: 'GET',
502 failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
503 success: function(response, opts) {
504 const res = response.result;
505 const subscription = !(!res || !res.data || res.data.status.toLowerCase() !== 'active');
506 vm.set('subscriptionActive', subscription);
507 },
508 });
509 },
510
511 updateStandardRepos: function(standardRepos) {
512 let me = this;
513 let vm = me.getViewModel();
514
515 let addButton = me.down('#addButton');
516 addButton.repoInfo = [];
517
518 for (const standardRepo of standardRepos) {
519 const handle = standardRepo.handle;
520 const status = standardRepo.status;
521
522 if (handle === "enterprise") {
523 vm.set('enterpriseRepo', status);
524 } else if (handle === "no-subscription") {
525 vm.set('noSubscriptionRepo', status);
526 }
527
528 addButton.repoInfo.push(standardRepo);
529 addButton.digest = me.digest;
530 }
531
532 addButton.setDisabled(false);
533 },
534
535 reload: function() {
536 let me = this;
537 let vm = me.getViewModel();
538 let repoGrid = me.down('proxmoxNodeAPTRepositoriesGrid');
539 let errorGrid = me.down('proxmoxNodeAPTRepositoriesErrors');
540
541 me.store.load(function(records, operation, success) {
542 let gridData = [];
543 let errors = [];
544 let digest;
545
546 if (success && records.length > 0) {
547 let data = records[0].data;
548 let files = data.files;
549 errors = data.errors;
550 digest = data.digest;
551
552 let infos = {};
553 for (const info of data.infos) {
554 let path = info.path;
555 let idx = info.index;
556
557 if (!infos[path]) {
558 infos[path] = {};
559 }
560 if (!infos[path][idx]) {
561 infos[path][idx] = {
562 origin: '',
563 warnings: [],
564 };
565 }
566
567 if (info.kind === 'origin') {
568 infos[path][idx].origin = info.message;
569 } else if (info.kind === 'warning' ||
570 (info.kind === 'ignore-pre-upgrade-warning' && !repoGrid.majorUpgradeAllowed)
571 ) {
572 infos[path][idx].warnings.push(info);
573 } else {
574 throw 'unknown info';
575 }
576 }
577
578
579 files.forEach(function(file) {
580 for (let n = 0; n < file.repositories.length; n++) {
581 let repo = file.repositories[n];
582 repo.Path = file.path;
583 repo.Index = n;
584 if (infos[file.path] && infos[file.path][n]) {
585 repo.Origin = infos[file.path][n].origin || Proxmox.Utils.UnknownText;
586 repo.warnings = infos[file.path][n].warnings || [];
587 }
588 gridData.push(repo);
589 }
590 });
591
592 repoGrid.store.loadData(gridData);
593
594 me.updateStandardRepos(data['standard-repos']);
595 }
596
597 me.digest = digest;
598
599 vm.set('errorCount', errors.length);
600 errorGrid.store.loadData(errors);
601 });
602
603 me.check_subscription();
604 },
605
606 listeners: {
607 activate: function() {
608 let me = this;
609 me.reload();
610 },
611 },
612
613 initComponent: function() {
614 let me = this;
615
616 if (!me.nodename) {
617 throw "no node name specified";
618 }
619
620 let store = Ext.create('Ext.data.Store', {
621 proxy: {
622 type: 'proxmox',
623 url: `/api2/json/nodes/${me.nodename}/apt/repositories`,
624 },
625 });
626
627 Ext.apply(me, { store: store });
628
629 Proxmox.Utils.monStoreErrors(me, me.store, true);
630
631 me.callParent();
632
633 me.getViewModel().set('product', me.product);
634 },
635 });