]> git.proxmox.com Git - proxmox-widget-toolkit.git/blob - src/node/APTRepositories.js
apt repos: make add-repo a checked command
[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 'OfficialHost',
7 'FileType',
8 'Enabled',
9 'Comment',
10 'Types',
11 'URIs',
12 'Suites',
13 'Components',
14 'Options',
15 ],
16 });
17
18 Ext.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',
36 renderer: value => `<i class='pve-grid-fa fa fa-fw fa-exclamation-triangle'></i>${value}`,
37 width: 350,
38 },
39 {
40 header: gettext('Error'),
41 dataIndex: 'error',
42 flex: 1,
43 },
44 ],
45 });
46
47 Ext.define('Proxmox.node.APTRepositoriesGrid', {
48 extend: 'Ext.grid.GridPanel',
49 xtype: 'proxmoxNodeAPTRepositoriesGrid',
50
51 title: gettext('APT Repositories'),
52
53 cls: 'proxmox-apt-repos', // to allow applying styling to general components with local effect
54
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 },
64 {
65 text: gettext('Add'),
66 menu: {
67 plain: true,
68 itemId: "addMenu",
69 items: [],
70 },
71 },
72 '-',
73 {
74 xtype: 'proxmoxButton',
75 text: gettext('Enable'),
76 defaultText: gettext('Enable'),
77 altText: gettext('Disable'),
78 id: 'repoEnableButton',
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 },
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 },
119 },
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' ||
233 (info.kind === 'ignore-pre-upgrade-warning' && !me.majorUpgradeAllowed)
234 ) {
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 },
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 },
312 });
313
314 Ext.define('Proxmox.node.APTRepositories', {
315 extend: 'Ext.panel.Panel',
316 xtype: 'proxmoxNodeAPTRepositories',
317 mixins: ['Proxmox.Mixin.CBind'],
318
319 digest: undefined,
320
321 viewModel: {
322 data: {
323 errorCount: 0,
324 subscriptionActive: '',
325 noSubscriptionRepo: '',
326 enterpriseRepo: '',
327 },
328 formulas: {
329 noErrors: (get) => get('errorCount') === 0,
330 mainWarning: function(get) {
331 // Not yet initialized
332 if (get('subscriptionActive') === '' ||
333 get('enterpriseRepo') === '') {
334 return '';
335 }
336
337 let withStyle = (msg) => "<div style='color:red;'><i class='fa fa-fw " +
338 "fa-exclamation-triangle'></i>" + gettext('Warning') + ': ' + msg + "</div>";
339
340 if (!get('subscriptionActive') && get('enterpriseRepo')) {
341 return withStyle(gettext('The enterprise repository is ' +
342 'enabled, but there is no active subscription!'));
343 }
344
345 if (get('noSubscriptionRepo')) {
346 return withStyle(gettext('The no-subscription repository is ' +
347 'not recommended for production use!'));
348 }
349
350 if (!get('enterpriseRepo') && !get('noSubscriptionRepo')) {
351 return withStyle(gettext('No Proxmox repository is enabled!'));
352 }
353
354 return '';
355 },
356 },
357 },
358
359 items: [
360 {
361 title: gettext('Warning'),
362 name: 'repositoriesMainWarning',
363 xtype: 'panel',
364 bind: {
365 title: '{mainWarning}',
366 hidden: '{!mainWarning}',
367 },
368 },
369 {
370 xtype: 'proxmoxNodeAPTRepositoriesErrors',
371 name: 'repositoriesErrors',
372 hidden: true,
373 bind: {
374 hidden: '{noErrors}',
375 },
376 },
377 {
378 xtype: 'proxmoxNodeAPTRepositoriesGrid',
379 name: 'repositoriesGrid',
380 cbind: {
381 nodename: '{nodename}',
382 },
383 majorUpgradeAllowed: false, // TODO get release information from an API call?
384 onSelectionChange: function(rec, sm) {
385 let me = this;
386 if (rec) {
387 let btn = me.up('proxmoxNodeAPTRepositories').down('#repoEnableButton');
388 btn.setText(rec.get('Enabled') ? gettext('Disable') : gettext('Enable'));
389 }
390 },
391 },
392 ],
393
394 check_subscription: function() {
395 let me = this;
396 let vm = me.getViewModel();
397
398 Proxmox.Utils.API2Request({
399 url: `/nodes/${me.nodename}/subscription`,
400 method: 'GET',
401 failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
402 success: function(response, opts) {
403 const res = response.result;
404 const subscription = !(!res || !res.data || res.data.status.toLowerCase() !== 'active');
405 vm.set('subscriptionActive', subscription);
406 },
407 });
408 },
409
410 updateStandardRepos: function(standardRepos) {
411 let me = this;
412 let vm = me.getViewModel();
413
414 let menu = me.down('#addMenu');
415 menu.removeAll();
416
417 for (const standardRepo of standardRepos) {
418 const handle = standardRepo.handle;
419 const status = standardRepo.status;
420
421 if (handle === "enterprise") {
422 vm.set('enterpriseRepo', status);
423 } else if (handle === "no-subscription") {
424 vm.set('noSubscriptionRepo', status);
425 }
426
427 let status_text = '';
428 if (status !== undefined && status !== null) {
429 status_text = Ext.String.format(
430 ' ({0}, {1})',
431 gettext('configured'),
432 status ? gettext('enabled') : gettext('disabled'),
433 );
434 }
435
436 menu.add({
437 text: standardRepo.name + status_text,
438 disabled: status !== undefined && status !== null,
439 repoHandle: handle,
440 handler: function(menuItem) {
441 Proxmox.Utils.checked_command(() => {
442 let params = {
443 handle: menuItem.repoHandle,
444 };
445
446 if (me.digest !== undefined) {
447 params.digest = me.digest;
448 }
449
450 Proxmox.Utils.API2Request({
451 url: `/nodes/${me.nodename}/apt/repositories`,
452 method: 'PUT',
453 params: params,
454 failure: function(response, opts) {
455 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
456 me.reload();
457 },
458 success: function(response, opts) {
459 me.reload();
460 },
461 });
462 });
463 },
464 });
465 }
466 },
467
468 reload: function() {
469 let me = this;
470 let vm = me.getViewModel();
471 let repoGrid = me.down('proxmoxNodeAPTRepositoriesGrid');
472 let errorGrid = me.down('proxmoxNodeAPTRepositoriesErrors');
473
474 me.store.load(function(records, operation, success) {
475 let gridData = [];
476 let errors = [];
477 let digest;
478
479 if (success && records.length > 0) {
480 let data = records[0].data;
481 let files = data.files;
482 errors = data.errors;
483 digest = data.digest;
484
485 files.forEach(function(file) {
486 for (let n = 0; n < file.repositories.length; n++) {
487 let repo = file.repositories[n];
488 repo.Path = file.path;
489 repo.Index = n;
490 gridData.push(repo);
491 }
492 });
493
494 repoGrid.addAdditionalInfos(gridData, data.infos);
495 repoGrid.store.loadData(gridData);
496
497 me.updateStandardRepos(data['standard-repos']);
498 }
499
500 me.digest = digest;
501
502 vm.set('errorCount', errors.length);
503 errorGrid.store.loadData(errors);
504 });
505
506 me.check_subscription();
507 },
508
509 listeners: {
510 activate: function() {
511 let me = this;
512 me.reload();
513 },
514 },
515
516 initComponent: function() {
517 let me = this;
518
519 if (!me.nodename) {
520 throw "no node name specified";
521 }
522
523 let store = Ext.create('Ext.data.Store', {
524 proxy: {
525 type: 'proxmox',
526 url: `/api2/json/nodes/${me.nodename}/apt/repositories`,
527 },
528 });
529
530 Ext.apply(me, { store: store });
531
532 Proxmox.Utils.monStoreErrors(me, me.store, true);
533
534 me.callParent();
535 },
536 });