]> git.proxmox.com Git - proxmox-widget-toolkit.git/blame - src/node/APTRepositories.js
apt repos: add product config-option and rework warning renderer
[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 {
126 header: gettext('Official'),
127 dataIndex: 'OfficialHost',
128 renderer: function(value, cell, record) {
129 let icon = (cls) => `<i class="fa fa-fw ${cls}"></i>`;
130
131 const enabled = record.data.Enabled;
132
133 if (value === undefined || value === null) {
134 return icon('fa-question-circle-o');
135 }
136 if (!value) {
137 return icon('fa-times ' + (enabled ? 'critical' : 'faded'));
138 }
139 return icon('fa-check ' + (enabled ? 'good' : 'faded'));
140 },
141 width: 70,
142 },
143 {
144 header: gettext('Enabled'),
145 dataIndex: 'Enabled',
146 renderer: Proxmox.Utils.format_enabled_toggle,
147 width: 90,
148 },
149 {
150 header: gettext('Types'),
151 dataIndex: 'Types',
152 renderer: function(types, cell, record) {
153 return types.join(' ');
154 },
155 width: 100,
156 },
157 {
158 header: gettext('URIs'),
159 dataIndex: 'URIs',
160 renderer: function(uris, cell, record) {
161 return uris.join(' ');
162 },
163 width: 350,
164 },
165 {
166 header: gettext('Suites'),
167 dataIndex: 'Suites',
168 renderer: function(suites, cell, record) {
169 return suites.join(' ');
170 },
171 width: 130,
172 },
173 {
174 header: gettext('Components'),
175 dataIndex: 'Components',
176 renderer: function(components, cell, record) {
177 return components.join(' ');
178 },
179 width: 170,
180 },
181 {
182 header: gettext('Options'),
183 dataIndex: 'Options',
184 renderer: function(options, cell, record) {
185 if (!options) {
186 return '';
187 }
188
189 let filetype = record.data.FileType;
190 let text = '';
191
192 options.forEach(function(option) {
193 let key = option.Key;
194 if (filetype === 'list') {
195 let values = option.Values.join(',');
196 text += `${key}=${values} `;
197 } else if (filetype === 'sources') {
198 let values = option.Values.join(' ');
199 text += `${key}: ${values}<br>`;
200 } else {
201 throw "unkown file type";
202 }
203 });
204 return text;
205 },
206 flex: 1,
207 },
208 {
209 header: gettext('Comment'),
210 dataIndex: 'Comment',
211 flex: 2,
212 },
213 ],
214
215 addAdditionalInfos: function(gridData, infos) {
216 let me = this;
217
218 let warnings = {};
219 let officialHosts = {};
220
221 let addLine = function(obj, key, line) {
222 if (obj[key]) {
223 obj[key] += "\n";
224 obj[key] += line;
225 } else {
226 obj[key] = line;
227 }
228 };
229
230 for (const info of infos) {
231 const key = `${info.path}:${info.index}`;
232 if (info.kind === 'warning' ||
af48de6b
TL
233 (info.kind === 'ignore-pre-upgrade-warning' && !me.majorUpgradeAllowed)
234 ) {
24313a9d
FE
235 addLine(warnings, key, gettext('Warning') + ": " + info.message);
236 } else if (info.kind === 'badge' && info.message === 'official host name') {
237 officialHosts[key] = true;
238 }
239 }
240
241 gridData.forEach(function(record) {
242 const key = `${record.Path}:${record.Index}`;
243 record.OfficialHost = !!officialHosts[key];
244 });
245
246 me.rowBodyFeature.getAdditionalData = function(innerData, rowIndex, record, orig) {
247 let headerCt = this.view.headerCt;
248 let colspan = headerCt.getColumnCount();
249
250 const key = `${innerData.Path}:${innerData.Index}`;
251 const warning_text = warnings[key];
252
253 return {
254 rowBody: '<div style="color: red; white-space: pre-line">' +
255 Ext.String.htmlEncode(warning_text) + '</div>',
256 rowBodyCls: warning_text ? '' : Ext.baseCSSPrefix + 'grid-row-body-hidden',
257 rowBodyColspan: colspan,
258 };
259 };
260 },
261
262 initComponent: function() {
263 let me = this;
264
265 if (!me.nodename) {
266 throw "no node name specified";
267 }
268
269 let store = Ext.create('Ext.data.Store', {
270 model: 'apt-repolist',
271 groupField: 'Path',
272 sorters: [
273 {
274 property: 'Index',
275 direction: 'ASC',
276 },
277 ],
278 });
279
280 let rowBodyFeature = Ext.create('Ext.grid.feature.RowBody', {});
281
282 let groupingFeature = Ext.create('Ext.grid.feature.Grouping', {
283 groupHeaderTpl: '{[ "File: " + values.name ]} ({rows.length} ' +
284 'repositor{[values.rows.length > 1 ? "ies" : "y"]})',
285 enableGroupingMenu: false,
286 });
287
288 let sm = Ext.create('Ext.selection.RowModel', {});
289
290 Ext.apply(me, {
291 store: store,
292 selModel: sm,
293 rowBodyFeature: rowBodyFeature,
294 features: [groupingFeature, rowBodyFeature],
295 });
296
297 me.callParent();
298 },
bb64cd03
TL
299
300 listeners: {
301 selectionchange: function() {
302 let me = this;
303
304 if (me.onSelectionChange) {
305 let sm = me.getSelectionModel();
306 let rec = sm.getSelection()[0];
307
308 me.onSelectionChange(rec, sm);
309 }
310 },
311 },
24313a9d
FE
312});
313
314Ext.define('Proxmox.node.APTRepositories', {
315 extend: 'Ext.panel.Panel',
24313a9d
FE
316 xtype: 'proxmoxNodeAPTRepositories',
317 mixins: ['Proxmox.Mixin.CBind'],
318
319 digest: undefined,
320
3fc020f4
TL
321 product: 'Proxmox VE', // default
322
24313a9d
FE
323 viewModel: {
324 data: {
3fc020f4 325 product: 'Proxmox VE', // default
24313a9d
FE
326 errorCount: 0,
327 subscriptionActive: '',
328 noSubscriptionRepo: '',
329 enterpriseRepo: '',
330 },
331 formulas: {
332 noErrors: (get) => get('errorCount') === 0,
333 mainWarning: function(get) {
334 // Not yet initialized
335 if (get('subscriptionActive') === '' ||
336 get('enterpriseRepo') === '') {
337 return '';
338 }
339
3fc020f4
TL
340 let icon = `<i class='fa fa-fw fa-exclamation-triangle critical'></i>`;
341 let fmt = (msg) => `<div class="black">${icon}${gettext('Warning')}: ${msg}</div>`;
24313a9d
FE
342
343 if (!get('subscriptionActive') && get('enterpriseRepo')) {
3fc020f4 344 return fmt(gettext('The enterprise repository is enabled, but there is no active subscription!'));
24313a9d
FE
345 }
346
347 if (get('noSubscriptionRepo')) {
3fc020f4 348 return fmt(gettext('The no-subscription repository is not recommended for production use!'));
24313a9d
FE
349 }
350
351 if (!get('enterpriseRepo') && !get('noSubscriptionRepo')) {
3fc020f4
TL
352 let msg = Ext.String.format(gettext('No {0} repository is enabled!'), get('product'));
353 return fmt(msg);
24313a9d
FE
354 }
355
356 return '';
357 },
358 },
359 },
360
361 items: [
362 {
363 title: gettext('Warning'),
364 name: 'repositoriesMainWarning',
365 xtype: 'panel',
366 bind: {
367 title: '{mainWarning}',
368 hidden: '{!mainWarning}',
369 },
370 },
371 {
372 xtype: 'proxmoxNodeAPTRepositoriesErrors',
373 name: 'repositoriesErrors',
374 hidden: true,
375 bind: {
376 hidden: '{noErrors}',
377 },
378 },
379 {
380 xtype: 'proxmoxNodeAPTRepositoriesGrid',
381 name: 'repositoriesGrid',
382 cbind: {
383 nodename: '{nodename}',
384 },
385 majorUpgradeAllowed: false, // TODO get release information from an API call?
bb64cd03
TL
386 onSelectionChange: function(rec, sm) {
387 let me = this;
388 if (rec) {
389 let btn = me.up('proxmoxNodeAPTRepositories').down('#repoEnableButton');
390 btn.setText(rec.get('Enabled') ? gettext('Disable') : gettext('Enable'));
391 }
392 },
24313a9d
FE
393 },
394 ],
395
396 check_subscription: function() {
397 let me = this;
398 let vm = me.getViewModel();
399
400 Proxmox.Utils.API2Request({
401 url: `/nodes/${me.nodename}/subscription`,
402 method: 'GET',
af48de6b 403 failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
24313a9d
FE
404 success: function(response, opts) {
405 const res = response.result;
af48de6b 406 const subscription = !(!res || !res.data || res.data.status.toLowerCase() !== 'active');
24313a9d
FE
407 vm.set('subscriptionActive', subscription);
408 },
409 });
410 },
411
412 updateStandardRepos: function(standardRepos) {
413 let me = this;
414 let vm = me.getViewModel();
415
d76eedb4
FE
416 let menu = me.down('#addMenu');
417 menu.removeAll();
418
24313a9d
FE
419 for (const standardRepo of standardRepos) {
420 const handle = standardRepo.handle;
421 const status = standardRepo.status;
422
423 if (handle === "enterprise") {
424 vm.set('enterpriseRepo', status);
425 } else if (handle === "no-subscription") {
426 vm.set('noSubscriptionRepo', status);
427 }
d76eedb4
FE
428
429 let status_text = '';
430 if (status !== undefined && status !== null) {
431 status_text = Ext.String.format(
432 ' ({0}, {1})',
433 gettext('configured'),
434 status ? gettext('enabled') : gettext('disabled'),
435 );
436 }
437
438 menu.add({
439 text: standardRepo.name + status_text,
440 disabled: status !== undefined && status !== null,
441 repoHandle: handle,
442 handler: function(menuItem) {
0e79ce21
TL
443 Proxmox.Utils.checked_command(() => {
444 let params = {
445 handle: menuItem.repoHandle,
446 };
447
448 if (me.digest !== undefined) {
449 params.digest = me.digest;
450 }
451
452 Proxmox.Utils.API2Request({
453 url: `/nodes/${me.nodename}/apt/repositories`,
454 method: 'PUT',
455 params: params,
456 failure: function(response, opts) {
457 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
458 me.reload();
459 },
460 success: function(response, opts) {
461 me.reload();
462 },
463 });
d76eedb4
FE
464 });
465 },
466 });
24313a9d
FE
467 }
468 },
469
470 reload: function() {
471 let me = this;
472 let vm = me.getViewModel();
473 let repoGrid = me.down('proxmoxNodeAPTRepositoriesGrid');
474 let errorGrid = me.down('proxmoxNodeAPTRepositoriesErrors');
475
476 me.store.load(function(records, operation, success) {
477 let gridData = [];
478 let errors = [];
479 let digest;
480
481 if (success && records.length > 0) {
482 let data = records[0].data;
483 let files = data.files;
484 errors = data.errors;
485 digest = data.digest;
486
487 files.forEach(function(file) {
488 for (let n = 0; n < file.repositories.length; n++) {
489 let repo = file.repositories[n];
490 repo.Path = file.path;
491 repo.Index = n;
492 gridData.push(repo);
493 }
494 });
495
496 repoGrid.addAdditionalInfos(gridData, data.infos);
497 repoGrid.store.loadData(gridData);
498
499 me.updateStandardRepos(data['standard-repos']);
500 }
501
502 me.digest = digest;
503
504 vm.set('errorCount', errors.length);
505 errorGrid.store.loadData(errors);
506 });
507
508 me.check_subscription();
509 },
510
511 listeners: {
512 activate: function() {
513 let me = this;
514 me.reload();
515 },
516 },
517
518 initComponent: function() {
519 let me = this;
520
521 if (!me.nodename) {
522 throw "no node name specified";
523 }
524
525 let store = Ext.create('Ext.data.Store', {
526 proxy: {
527 type: 'proxmox',
528 url: `/api2/json/nodes/${me.nodename}/apt/repositories`,
529 },
530 });
531
532 Ext.apply(me, { store: store });
533
534 Proxmox.Utils.monStoreErrors(me, me.store, true);
535
536 me.callParent();
3fc020f4
TL
537
538 me.getViewModel().set('product', me.product);
24313a9d
FE
539 },
540});