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