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