]> git.proxmox.com Git - proxmox-widget-toolkit.git/blame - src/node/APTRepositories.js
apt repos: move Official to the right & use checkbox to render enabled
[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',
6 'OfficialHost',
7 'FileType',
8 'Enabled',
9 'Comment',
10 'Types',
11 'URIs',
12 'Suites',
13 'Components',
14 'Options',
15 ],
16});
17
18Ext.define('Proxmox.node.APTRepositoriesErrors', {
19 extend: 'Ext.grid.GridPanel',
20
21 xtype: 'proxmoxNodeAPTRepositoriesErrors',
22
23 title: gettext('Errors'),
24
25 store: {},
26
27 viewConfig: {
28 stripeRows: false,
29 getRowClass: () => 'proxmox-invalid-row',
30 },
31
32 columns: [
33 {
34 header: gettext('File'),
35 dataIndex: 'path',
af48de6b 36 renderer: value => `<i class='pve-grid-fa fa fa-fw fa-exclamation-triangle'></i>${value}`,
24313a9d
FE
37 width: 350,
38 },
39 {
40 header: gettext('Error'),
41 dataIndex: 'error',
42 flex: 1,
43 },
44 ],
45});
46
47Ext.define('Proxmox.node.APTRepositoriesGrid', {
48 extend: 'Ext.grid.GridPanel',
24313a9d
FE
49 xtype: 'proxmoxNodeAPTRepositoriesGrid',
50
51 title: gettext('APT Repositories'),
52
994fe897
TL
53 cls: 'proxmox-apt-repos', // to allow applying styling to general components with local effect
54
24313a9d
FE
55 tbar: [
56 {
57 text: gettext('Reload'),
58 iconCls: 'fa fa-refresh',
59 handler: function() {
60 let me = this;
61 me.up('proxmoxNodeAPTRepositories').reload();
62 },
63 },
d76eedb4
FE
64 {
65 text: gettext('Add'),
66 menu: {
67 plain: true,
68 itemId: "addMenu",
69 items: [],
70 },
71 },
af48de6b 72 '-',
d76eedb4
FE
73 {
74 xtype: 'proxmoxButton',
bb64cd03
TL
75 text: gettext('Enable'),
76 defaultText: gettext('Enable'),
77 altText: gettext('Disable'),
78 id: 'repoEnableButton',
d76eedb4
FE
79 disabled: true,
80 handler: function(button, event, record) {
81 let me = this;
82 let panel = me.up('proxmoxNodeAPTRepositories');
83
84 let params = {
85 path: record.data.Path,
86 index: record.data.Index,
87 enabled: record.data.Enabled ? 0 : 1, // invert
88 };
89
90 if (panel.digest !== undefined) {
91 params.digest = panel.digest;
92 }
93
94 Proxmox.Utils.API2Request({
95 url: `/nodes/${panel.nodename}/apt/repositories`,
96 method: 'POST',
97 params: params,
98 failure: function(response, opts) {
99 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
100 panel.reload();
101 },
102 success: function(response, opts) {
103 panel.reload();
104 },
105 });
106 },
bb64cd03
TL
107 listeners: {
108 render: function(btn) {
109 // HACK: calculate the max button width on first render to avoid toolbar glitches
110 let defSize = btn.getSize().width;
111
112 btn.setText(btn.altText);
113 let altSize = btn.getSize().width;
114
115 btn.setText(btn.defaultText);
116 btn.setSize({ width: altSize > defSize ? altSize : defSize });
117 },
118 },
d76eedb4 119 },
24313a9d
FE
120 ],
121
122 sortableColumns: false,
123
124 columns: [
125 {
03c4c65b 126 xtype: 'checkcolumn',
24313a9d
FE
127 header: gettext('Enabled'),
128 dataIndex: 'Enabled',
03c4c65b
TL
129 listeners: {
130 beforecheckchange: () => false, // veto, we don't want to allow inline change - to subtle
131 },
24313a9d
FE
132 width: 90,
133 },
134 {
135 header: gettext('Types'),
136 dataIndex: 'Types',
137 renderer: function(types, cell, record) {
138 return types.join(' ');
139 },
140 width: 100,
141 },
142 {
143 header: gettext('URIs'),
144 dataIndex: 'URIs',
145 renderer: function(uris, cell, record) {
146 return uris.join(' ');
147 },
148 width: 350,
149 },
150 {
151 header: gettext('Suites'),
152 dataIndex: 'Suites',
153 renderer: function(suites, cell, record) {
154 return suites.join(' ');
155 },
156 width: 130,
157 },
158 {
159 header: gettext('Components'),
160 dataIndex: 'Components',
161 renderer: function(components, cell, record) {
162 return components.join(' ');
163 },
164 width: 170,
165 },
166 {
167 header: gettext('Options'),
168 dataIndex: 'Options',
169 renderer: function(options, cell, record) {
170 if (!options) {
171 return '';
172 }
173
174 let filetype = record.data.FileType;
175 let text = '';
176
177 options.forEach(function(option) {
178 let key = option.Key;
179 if (filetype === 'list') {
180 let values = option.Values.join(',');
181 text += `${key}=${values} `;
182 } else if (filetype === 'sources') {
183 let values = option.Values.join(' ');
184 text += `${key}: ${values}<br>`;
185 } else {
186 throw "unkown file type";
187 }
188 });
189 return text;
190 },
191 flex: 1,
192 },
03c4c65b
TL
193 {
194 header: gettext('Official'),
195 dataIndex: 'OfficialHost',
196 renderer: function(value, cell, record) {
197 let icon = (cls) => `<i class="fa fa-fw ${cls}"></i>`;
198
199 if (value === undefined || value === null) {
200 return icon('fa-question-circle-o');
201 }
202 const enabled = record.data.Enabled;
203 if (!value) {
204 return icon('fa-question ' + (enabled ? 'warning' : 'faded'));
205 }
206 return icon('fa-check ' + (enabled ? 'good' : 'faded'));
207 },
208 width: 70,
209 },
24313a9d
FE
210 {
211 header: gettext('Comment'),
212 dataIndex: 'Comment',
213 flex: 2,
214 },
215 ],
216
217 addAdditionalInfos: function(gridData, infos) {
218 let me = this;
219
220 let warnings = {};
221 let officialHosts = {};
222
223 let addLine = function(obj, key, line) {
224 if (obj[key]) {
225 obj[key] += "\n";
226 obj[key] += line;
227 } else {
228 obj[key] = line;
229 }
230 };
231
232 for (const info of infos) {
233 const key = `${info.path}:${info.index}`;
234 if (info.kind === 'warning' ||
af48de6b
TL
235 (info.kind === 'ignore-pre-upgrade-warning' && !me.majorUpgradeAllowed)
236 ) {
24313a9d
FE
237 addLine(warnings, key, gettext('Warning') + ": " + info.message);
238 } else if (info.kind === 'badge' && info.message === 'official host name') {
239 officialHosts[key] = true;
240 }
241 }
242
243 gridData.forEach(function(record) {
244 const key = `${record.Path}:${record.Index}`;
245 record.OfficialHost = !!officialHosts[key];
246 });
247
248 me.rowBodyFeature.getAdditionalData = function(innerData, rowIndex, record, orig) {
249 let headerCt = this.view.headerCt;
250 let colspan = headerCt.getColumnCount();
251
252 const key = `${innerData.Path}:${innerData.Index}`;
253 const warning_text = warnings[key];
254
255 return {
256 rowBody: '<div style="color: red; white-space: pre-line">' +
257 Ext.String.htmlEncode(warning_text) + '</div>',
258 rowBodyCls: warning_text ? '' : Ext.baseCSSPrefix + 'grid-row-body-hidden',
259 rowBodyColspan: colspan,
260 };
261 };
262 },
263
264 initComponent: function() {
265 let me = this;
266
267 if (!me.nodename) {
268 throw "no node name specified";
269 }
270
271 let store = Ext.create('Ext.data.Store', {
272 model: 'apt-repolist',
273 groupField: 'Path',
274 sorters: [
275 {
276 property: 'Index',
277 direction: 'ASC',
278 },
279 ],
280 });
281
282 let rowBodyFeature = Ext.create('Ext.grid.feature.RowBody', {});
283
284 let groupingFeature = Ext.create('Ext.grid.feature.Grouping', {
285 groupHeaderTpl: '{[ "File: " + values.name ]} ({rows.length} ' +
286 'repositor{[values.rows.length > 1 ? "ies" : "y"]})',
287 enableGroupingMenu: false,
288 });
289
290 let sm = Ext.create('Ext.selection.RowModel', {});
291
292 Ext.apply(me, {
293 store: store,
294 selModel: sm,
295 rowBodyFeature: rowBodyFeature,
296 features: [groupingFeature, rowBodyFeature],
297 });
298
299 me.callParent();
300 },
bb64cd03
TL
301
302 listeners: {
303 selectionchange: function() {
304 let me = this;
305
306 if (me.onSelectionChange) {
307 let sm = me.getSelectionModel();
308 let rec = sm.getSelection()[0];
309
310 me.onSelectionChange(rec, sm);
311 }
312 },
313 },
24313a9d
FE
314});
315
316Ext.define('Proxmox.node.APTRepositories', {
317 extend: 'Ext.panel.Panel',
24313a9d
FE
318 xtype: 'proxmoxNodeAPTRepositories',
319 mixins: ['Proxmox.Mixin.CBind'],
320
321 digest: undefined,
322
3fc020f4
TL
323 product: 'Proxmox VE', // default
324
24313a9d
FE
325 viewModel: {
326 data: {
3fc020f4 327 product: 'Proxmox VE', // default
24313a9d
FE
328 errorCount: 0,
329 subscriptionActive: '',
330 noSubscriptionRepo: '',
331 enterpriseRepo: '',
332 },
333 formulas: {
334 noErrors: (get) => get('errorCount') === 0,
335 mainWarning: function(get) {
336 // Not yet initialized
337 if (get('subscriptionActive') === '' ||
338 get('enterpriseRepo') === '') {
339 return '';
340 }
341
3fc020f4
TL
342 let icon = `<i class='fa fa-fw fa-exclamation-triangle critical'></i>`;
343 let fmt = (msg) => `<div class="black">${icon}${gettext('Warning')}: ${msg}</div>`;
24313a9d
FE
344
345 if (!get('subscriptionActive') && get('enterpriseRepo')) {
3fc020f4 346 return fmt(gettext('The enterprise repository is enabled, but there is no active subscription!'));
24313a9d
FE
347 }
348
349 if (get('noSubscriptionRepo')) {
3fc020f4 350 return fmt(gettext('The no-subscription repository is not recommended for production use!'));
24313a9d
FE
351 }
352
353 if (!get('enterpriseRepo') && !get('noSubscriptionRepo')) {
3fc020f4
TL
354 let msg = Ext.String.format(gettext('No {0} repository is enabled!'), get('product'));
355 return fmt(msg);
24313a9d
FE
356 }
357
358 return '';
359 },
360 },
361 },
362
363 items: [
364 {
365 title: gettext('Warning'),
366 name: 'repositoriesMainWarning',
367 xtype: 'panel',
368 bind: {
369 title: '{mainWarning}',
370 hidden: '{!mainWarning}',
371 },
372 },
373 {
374 xtype: 'proxmoxNodeAPTRepositoriesErrors',
375 name: 'repositoriesErrors',
376 hidden: true,
377 bind: {
378 hidden: '{noErrors}',
379 },
380 },
381 {
382 xtype: 'proxmoxNodeAPTRepositoriesGrid',
383 name: 'repositoriesGrid',
384 cbind: {
385 nodename: '{nodename}',
386 },
387 majorUpgradeAllowed: false, // TODO get release information from an API call?
bb64cd03
TL
388 onSelectionChange: function(rec, sm) {
389 let me = this;
390 if (rec) {
391 let btn = me.up('proxmoxNodeAPTRepositories').down('#repoEnableButton');
392 btn.setText(rec.get('Enabled') ? gettext('Disable') : gettext('Enable'));
393 }
394 },
24313a9d
FE
395 },
396 ],
397
398 check_subscription: function() {
399 let me = this;
400 let vm = me.getViewModel();
401
402 Proxmox.Utils.API2Request({
403 url: `/nodes/${me.nodename}/subscription`,
404 method: 'GET',
af48de6b 405 failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
24313a9d
FE
406 success: function(response, opts) {
407 const res = response.result;
af48de6b 408 const subscription = !(!res || !res.data || res.data.status.toLowerCase() !== 'active');
24313a9d
FE
409 vm.set('subscriptionActive', subscription);
410 },
411 });
412 },
413
414 updateStandardRepos: function(standardRepos) {
415 let me = this;
416 let vm = me.getViewModel();
417
d76eedb4
FE
418 let menu = me.down('#addMenu');
419 menu.removeAll();
420
24313a9d
FE
421 for (const standardRepo of standardRepos) {
422 const handle = standardRepo.handle;
423 const status = standardRepo.status;
424
425 if (handle === "enterprise") {
426 vm.set('enterpriseRepo', status);
427 } else if (handle === "no-subscription") {
428 vm.set('noSubscriptionRepo', status);
429 }
d76eedb4
FE
430
431 let status_text = '';
432 if (status !== undefined && status !== null) {
433 status_text = Ext.String.format(
434 ' ({0}, {1})',
435 gettext('configured'),
436 status ? gettext('enabled') : gettext('disabled'),
437 );
438 }
439
440 menu.add({
441 text: standardRepo.name + status_text,
442 disabled: status !== undefined && status !== null,
443 repoHandle: handle,
444 handler: function(menuItem) {
0e79ce21
TL
445 Proxmox.Utils.checked_command(() => {
446 let params = {
447 handle: menuItem.repoHandle,
448 };
449
450 if (me.digest !== undefined) {
451 params.digest = me.digest;
452 }
453
454 Proxmox.Utils.API2Request({
455 url: `/nodes/${me.nodename}/apt/repositories`,
456 method: 'PUT',
457 params: params,
458 failure: function(response, opts) {
459 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
460 me.reload();
461 },
462 success: function(response, opts) {
463 me.reload();
464 },
465 });
d76eedb4
FE
466 });
467 },
468 });
24313a9d
FE
469 }
470 },
471
472 reload: function() {
473 let me = this;
474 let vm = me.getViewModel();
475 let repoGrid = me.down('proxmoxNodeAPTRepositoriesGrid');
476 let errorGrid = me.down('proxmoxNodeAPTRepositoriesErrors');
477
478 me.store.load(function(records, operation, success) {
479 let gridData = [];
480 let errors = [];
481 let digest;
482
483 if (success && records.length > 0) {
484 let data = records[0].data;
485 let files = data.files;
486 errors = data.errors;
487 digest = data.digest;
488
489 files.forEach(function(file) {
490 for (let n = 0; n < file.repositories.length; n++) {
491 let repo = file.repositories[n];
492 repo.Path = file.path;
493 repo.Index = n;
494 gridData.push(repo);
495 }
496 });
497
498 repoGrid.addAdditionalInfos(gridData, data.infos);
499 repoGrid.store.loadData(gridData);
500
501 me.updateStandardRepos(data['standard-repos']);
502 }
503
504 me.digest = digest;
505
506 vm.set('errorCount', errors.length);
507 errorGrid.store.loadData(errors);
508 });
509
510 me.check_subscription();
511 },
512
513 listeners: {
514 activate: function() {
515 let me = this;
516 me.reload();
517 },
518 },
519
520 initComponent: function() {
521 let me = this;
522
523 if (!me.nodename) {
524 throw "no node name specified";
525 }
526
527 let store = Ext.create('Ext.data.Store', {
528 proxy: {
529 type: 'proxmox',
530 url: `/api2/json/nodes/${me.nodename}/apt/repositories`,
531 },
532 });
533
534 Ext.apply(me, { store: store });
535
536 Proxmox.Utils.monStoreErrors(me, me.store, true);
537
538 me.callParent();
3fc020f4
TL
539
540 me.getViewModel().set('product', me.product);
24313a9d
FE
541 },
542});