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