]> git.proxmox.com Git - proxmox-widget-toolkit.git/blame - src/node/APTRepositories.js
node: repos: fallback to "Other" for unknown origin
[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',
d91987a5 6 'Origin',
24313a9d
FE
7 'FileType',
8 'Enabled',
9 'Comment',
10 'Types',
11 'URIs',
12 'Suites',
13 'Components',
14 'Options',
15 ],
16});
17
21860ea4
FE
18Ext.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'),
5e9eb245 26 width: 600,
21860ea4
FE
27
28 initComponent: function() {
29 let me = this;
30
31 if (!me.repoInfo || me.repoInfo.length === 0) {
32 throw "repository information not initialized";
33 }
34
35 let description = Ext.create('Ext.form.field.Display', {
36 fieldLabel: gettext('Description'),
37 name: 'description',
38 });
39
40 let status = Ext.create('Ext.form.field.Display', {
41 fieldLabel: gettext('Status'),
42 name: 'status',
43 renderer: function(value) {
44 let statusText = gettext('Not yet configured');
45 if (value !== '') {
46 statusText = Ext.String.format(
47 '{0}: {1}',
48 gettext('Configured'),
49 value ? gettext('enabled') : gettext('disabled'),
50 );
51 }
52
53 return statusText;
54 },
55 });
56
57 let repoSelector = Ext.create('Proxmox.form.KVComboBox', {
58 fieldLabel: gettext('Repository'),
59 xtype: 'proxmoxKVComboBox',
60 name: 'handle',
61 allowBlank: false,
62 comboItems: me.repoInfo.map(info => [info.handle, info.name]),
63 isValid: function() {
64 const handle = this.value;
65
66 if (!handle) {
67 return false;
68 }
69
70 const info = me.repoInfo.find(elem => elem.handle === handle);
71
72 if (!info) {
73 return false;
74 }
75
76 // not yet configured
77 return info.status === undefined || info.status === null;
78 },
79 listeners: {
80 change: function(f, value) {
81 const info = me.repoInfo.find(elem => elem.handle === value);
82 description.setValue(info.description);
83 status.setValue(info.status);
84 },
85 },
86 });
87
88 repoSelector.setValue(me.repoInfo[0].handle);
89
21860ea4 90 Ext.apply(me, {
5e9eb245
TL
91 items: [
92 repoSelector,
93 description,
94 status,
95 ],
21860ea4
FE
96 repoSelector: repoSelector,
97 });
98
99 me.callParent();
100 },
101});
102
24313a9d
FE
103Ext.define('Proxmox.node.APTRepositoriesErrors', {
104 extend: 'Ext.grid.GridPanel',
105
106 xtype: 'proxmoxNodeAPTRepositoriesErrors',
107
108 title: gettext('Errors'),
109
110 store: {},
111
d8b5cd80
DC
112 border: false,
113
24313a9d
FE
114 viewConfig: {
115 stripeRows: false,
116 getRowClass: () => 'proxmox-invalid-row',
117 },
118
119 columns: [
120 {
121 header: gettext('File'),
122 dataIndex: 'path',
af48de6b 123 renderer: value => `<i class='pve-grid-fa fa fa-fw fa-exclamation-triangle'></i>${value}`,
24313a9d
FE
124 width: 350,
125 },
126 {
127 header: gettext('Error'),
128 dataIndex: 'error',
129 flex: 1,
130 },
131 ],
132});
133
134Ext.define('Proxmox.node.APTRepositoriesGrid', {
135 extend: 'Ext.grid.GridPanel',
24313a9d
FE
136 xtype: 'proxmoxNodeAPTRepositoriesGrid',
137
138 title: gettext('APT Repositories'),
139
994fe897
TL
140 cls: 'proxmox-apt-repos', // to allow applying styling to general components with local effect
141
d8b5cd80
DC
142 border: false,
143
24313a9d
FE
144 tbar: [
145 {
146 text: gettext('Reload'),
147 iconCls: 'fa fa-refresh',
148 handler: function() {
149 let me = this;
150 me.up('proxmoxNodeAPTRepositories').reload();
151 },
152 },
d76eedb4
FE
153 {
154 text: gettext('Add'),
21860ea4
FE
155 id: 'addButton',
156 disabled: true,
157 repoInfo: undefined,
158 handler: function(button, event, record) {
159 Proxmox.Utils.checked_command(() => {
160 let me = this;
161 let panel = me.up('proxmoxNodeAPTRepositories');
162
163 let extraParams = {};
164 if (panel.digest !== undefined) {
165 extraParams.digest = panel.digest;
166 }
167
168 Ext.create('Proxmox.window.APTRepositoryAdd', {
169 repoInfo: me.repoInfo,
170 url: `/api2/json/nodes/${panel.nodename}/apt/repositories`,
171 method: 'PUT',
172 extraRequestParams: extraParams,
173 listeners: {
174 destroy: function() {
175 panel.reload();
176 },
177 },
178 }).show();
179 });
d76eedb4
FE
180 },
181 },
af48de6b 182 '-',
d76eedb4
FE
183 {
184 xtype: 'proxmoxButton',
bb64cd03
TL
185 text: gettext('Enable'),
186 defaultText: gettext('Enable'),
187 altText: gettext('Disable'),
188 id: 'repoEnableButton',
d76eedb4 189 disabled: true,
003c4982
DC
190 bind: {
191 text: '{enableButtonText}',
192 },
d76eedb4
FE
193 handler: function(button, event, record) {
194 let me = this;
195 let panel = me.up('proxmoxNodeAPTRepositories');
196
197 let params = {
198 path: record.data.Path,
199 index: record.data.Index,
200 enabled: record.data.Enabled ? 0 : 1, // invert
201 };
202
203 if (panel.digest !== undefined) {
204 params.digest = panel.digest;
205 }
206
207 Proxmox.Utils.API2Request({
208 url: `/nodes/${panel.nodename}/apt/repositories`,
209 method: 'POST',
210 params: params,
211 failure: function(response, opts) {
212 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
213 panel.reload();
214 },
215 success: function(response, opts) {
216 panel.reload();
217 },
218 });
219 },
bb64cd03
TL
220 listeners: {
221 render: function(btn) {
222 // HACK: calculate the max button width on first render to avoid toolbar glitches
223 let defSize = btn.getSize().width;
224
225 btn.setText(btn.altText);
226 let altSize = btn.getSize().width;
227
228 btn.setText(btn.defaultText);
229 btn.setSize({ width: altSize > defSize ? altSize : defSize });
230 },
231 },
d76eedb4 232 },
24313a9d
FE
233 ],
234
235 sortableColumns: false,
205d2751
TL
236 viewConfig: {
237 stripeRows: false,
238 getRowClass: (record, index) => record.get('Enabled') ? '' : 'proxmox-disabled-row',
239 },
24313a9d
FE
240
241 columns: [
242 {
03c4c65b 243 xtype: 'checkcolumn',
24313a9d
FE
244 header: gettext('Enabled'),
245 dataIndex: 'Enabled',
03c4c65b
TL
246 listeners: {
247 beforecheckchange: () => false, // veto, we don't want to allow inline change - to subtle
248 },
24313a9d
FE
249 width: 90,
250 },
251 {
252 header: gettext('Types'),
253 dataIndex: 'Types',
254 renderer: function(types, cell, record) {
255 return types.join(' ');
256 },
257 width: 100,
258 },
259 {
260 header: gettext('URIs'),
261 dataIndex: 'URIs',
262 renderer: function(uris, cell, record) {
263 return uris.join(' ');
264 },
265 width: 350,
266 },
267 {
268 header: gettext('Suites'),
269 dataIndex: 'Suites',
e71fc6e4
DC
270 renderer: function(suites, metaData, record) {
271 let err = '';
272 if (record.data.warnings && record.data.warnings.length > 0) {
273 let txt = [gettext('Warning')];
274 record.data.warnings.forEach((warning) => {
275 if (warning.property === 'Suites') {
276 txt.push(warning.message);
277 }
278 });
279 metaData.tdAttr = `data-qtip="${Ext.htmlEncode(txt.join('<br>'))}"`;
5a1fddb6
TL
280 if (record.data.Enabled) {
281 metaData.tdCls = 'proxmox-invalid-row';
282 err = '<i class="fa fa-fw critical fa-exclamation-circle"></i> ';
283 } else {
284 metaData.tdCls = 'proxmox-warning-row';
285 err = '<i class="fa fa-fw warning fa-exclamation-circle"></i> ';
286 }
e71fc6e4
DC
287 }
288 return suites.join(' ') + err;
24313a9d
FE
289 },
290 width: 130,
291 },
292 {
293 header: gettext('Components'),
294 dataIndex: 'Components',
295 renderer: function(components, cell, record) {
296 return components.join(' ');
297 },
298 width: 170,
299 },
300 {
301 header: gettext('Options'),
302 dataIndex: 'Options',
303 renderer: function(options, cell, record) {
304 if (!options) {
305 return '';
306 }
307
308 let filetype = record.data.FileType;
309 let text = '';
310
311 options.forEach(function(option) {
312 let key = option.Key;
313 if (filetype === 'list') {
314 let values = option.Values.join(',');
315 text += `${key}=${values} `;
316 } else if (filetype === 'sources') {
317 let values = option.Values.join(' ');
318 text += `${key}: ${values}<br>`;
319 } else {
320 throw "unkown file type";
321 }
322 });
323 return text;
324 },
325 flex: 1,
326 },
03c4c65b 327 {
d91987a5
FE
328 header: gettext('Origin'),
329 dataIndex: 'Origin',
036f48c1
TL
330 width: 120,
331 renderer: (value, meta, rec) => {
f0966f29
TL
332 if (typeof value !== 'string' || value.length === 0) {
333 value = gettext('Other');
334 }
036f48c1
TL
335 let cls = 'fa fa-fw fa-question-circle-o';
336 if (value.match(/^\s*Proxmox\s*$/i)) {
337 cls = 'pmx-itype-icon pmx-itype-icon-proxmox-x';
338 } else if (value.match(/^\s*Debian\s*$/i)) {
339 cls = 'pmx-itype-icon pmx-itype-icon-debian-swirl';
340 }
341 return `<i class='${cls}'></i> ${value}`;
342 },
03c4c65b 343 },
24313a9d
FE
344 {
345 header: gettext('Comment'),
346 dataIndex: 'Comment',
347 flex: 2,
348 },
349 ],
350
24313a9d
FE
351 initComponent: function() {
352 let me = this;
353
354 if (!me.nodename) {
355 throw "no node name specified";
356 }
357
358 let store = Ext.create('Ext.data.Store', {
359 model: 'apt-repolist',
360 groupField: 'Path',
361 sorters: [
362 {
363 property: 'Index',
364 direction: 'ASC',
365 },
366 ],
367 });
368
24313a9d
FE
369 let groupingFeature = Ext.create('Ext.grid.feature.Grouping', {
370 groupHeaderTpl: '{[ "File: " + values.name ]} ({rows.length} ' +
371 'repositor{[values.rows.length > 1 ? "ies" : "y"]})',
372 enableGroupingMenu: false,
373 });
374
24313a9d
FE
375 Ext.apply(me, {
376 store: store,
e71fc6e4 377 features: [groupingFeature],
24313a9d
FE
378 });
379
380 me.callParent();
381 },
382});
383
384Ext.define('Proxmox.node.APTRepositories', {
385 extend: 'Ext.panel.Panel',
24313a9d
FE
386 xtype: 'proxmoxNodeAPTRepositories',
387 mixins: ['Proxmox.Mixin.CBind'],
388
389 digest: undefined,
390
3fc020f4
TL
391 product: 'Proxmox VE', // default
392
003c4982
DC
393 controller: {
394 xclass: 'Ext.app.ViewController',
395
396 selectionChange: function(grid, selection) {
397 let me = this;
398 if (!selection || selection.length < 1) {
399 return;
400 }
401 let rec = selection[0];
402 let vm = me.getViewModel();
403 vm.set('selectionenabled', rec.get('Enabled'));
404 },
405 },
406
24313a9d
FE
407 viewModel: {
408 data: {
3fc020f4 409 product: 'Proxmox VE', // default
24313a9d
FE
410 errorCount: 0,
411 subscriptionActive: '',
412 noSubscriptionRepo: '',
413 enterpriseRepo: '',
003c4982 414 selectionenabled: false,
24313a9d
FE
415 },
416 formulas: {
417 noErrors: (get) => get('errorCount') === 0,
003c4982
DC
418 enableButtonText: (get) => get('selectionenabled')
419 ? gettext('Disable') : gettext('Enable'),
24313a9d
FE
420 mainWarning: function(get) {
421 // Not yet initialized
422 if (get('subscriptionActive') === '' ||
423 get('enterpriseRepo') === '') {
424 return '';
425 }
426
3fc020f4
TL
427 let icon = `<i class='fa fa-fw fa-exclamation-triangle critical'></i>`;
428 let fmt = (msg) => `<div class="black">${icon}${gettext('Warning')}: ${msg}</div>`;
24313a9d
FE
429
430 if (!get('subscriptionActive') && get('enterpriseRepo')) {
3fc020f4 431 return fmt(gettext('The enterprise repository is enabled, but there is no active subscription!'));
24313a9d
FE
432 }
433
434 if (get('noSubscriptionRepo')) {
3fc020f4 435 return fmt(gettext('The no-subscription repository is not recommended for production use!'));
24313a9d
FE
436 }
437
438 if (!get('enterpriseRepo') && !get('noSubscriptionRepo')) {
3fc020f4
TL
439 let msg = Ext.String.format(gettext('No {0} repository is enabled!'), get('product'));
440 return fmt(msg);
24313a9d
FE
441 }
442
443 return '';
444 },
445 },
446 },
447
82071150
DC
448 scrollable: true,
449 layout: {
450 type: 'vbox',
451 align: 'stretch',
452 },
453
24313a9d
FE
454 items: [
455 {
d8b5cd80
DC
456 xtype: 'header',
457 baseCls: 'x-panel-header',
24313a9d 458 bind: {
d8b5cd80 459 hidden: '{!mainWarning}',
24313a9d 460 title: '{mainWarning}',
d8b5cd80
DC
461 },
462 },
463 {
464 xtype: 'box',
465 bind: {
24313a9d
FE
466 hidden: '{!mainWarning}',
467 },
d8b5cd80 468 height: 5,
24313a9d
FE
469 },
470 {
471 xtype: 'proxmoxNodeAPTRepositoriesErrors',
472 name: 'repositoriesErrors',
473 hidden: true,
d8b5cd80 474 padding: '0 0 5 0',
24313a9d
FE
475 bind: {
476 hidden: '{noErrors}',
477 },
478 },
479 {
480 xtype: 'proxmoxNodeAPTRepositoriesGrid',
481 name: 'repositoriesGrid',
482 cbind: {
483 nodename: '{nodename}',
484 },
485 majorUpgradeAllowed: false, // TODO get release information from an API call?
003c4982
DC
486 listeners: {
487 selectionchange: 'selectionChange',
bb64cd03 488 },
24313a9d
FE
489 },
490 ],
491
492 check_subscription: function() {
493 let me = this;
494 let vm = me.getViewModel();
495
496 Proxmox.Utils.API2Request({
497 url: `/nodes/${me.nodename}/subscription`,
498 method: 'GET',
af48de6b 499 failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
24313a9d
FE
500 success: function(response, opts) {
501 const res = response.result;
af48de6b 502 const subscription = !(!res || !res.data || res.data.status.toLowerCase() !== 'active');
24313a9d
FE
503 vm.set('subscriptionActive', subscription);
504 },
505 });
506 },
507
508 updateStandardRepos: function(standardRepos) {
509 let me = this;
510 let vm = me.getViewModel();
511
21860ea4
FE
512 let addButton = me.down('#addButton');
513 addButton.repoInfo = [];
d76eedb4 514
24313a9d
FE
515 for (const standardRepo of standardRepos) {
516 const handle = standardRepo.handle;
517 const status = standardRepo.status;
518
519 if (handle === "enterprise") {
520 vm.set('enterpriseRepo', status);
521 } else if (handle === "no-subscription") {
522 vm.set('noSubscriptionRepo', status);
523 }
d76eedb4 524
21860ea4
FE
525 addButton.repoInfo.push(standardRepo);
526 addButton.digest = me.digest;
24313a9d 527 }
21860ea4
FE
528
529 addButton.setDisabled(false);
24313a9d
FE
530 },
531
532 reload: function() {
533 let me = this;
534 let vm = me.getViewModel();
535 let repoGrid = me.down('proxmoxNodeAPTRepositoriesGrid');
536 let errorGrid = me.down('proxmoxNodeAPTRepositoriesErrors');
537
538 me.store.load(function(records, operation, success) {
539 let gridData = [];
540 let errors = [];
541 let digest;
542
543 if (success && records.length > 0) {
544 let data = records[0].data;
545 let files = data.files;
546 errors = data.errors;
547 digest = data.digest;
548
e71fc6e4
DC
549 let infos = {};
550 for (const info of data.infos) {
551 let path = info.path;
552 let idx = info.index;
553
554 if (!infos[path]) {
555 infos[path] = {};
556 }
557 if (!infos[path][idx]) {
558 infos[path][idx] = {
559 origin: '',
560 warnings: [],
561 };
562 }
563
564 if (info.kind === 'origin') {
565 infos[path][idx].origin = info.message;
566 } else if (info.kind === 'warning' ||
567 (info.kind === 'ignore-pre-upgrade-warning' && !repoGrid.majorUpgradeAllowed)
568 ) {
569 infos[path][idx].warnings.push(info);
570 } else {
571 throw 'unknown info';
572 }
573 }
574
575
24313a9d
FE
576 files.forEach(function(file) {
577 for (let n = 0; n < file.repositories.length; n++) {
578 let repo = file.repositories[n];
579 repo.Path = file.path;
580 repo.Index = n;
e71fc6e4
DC
581 if (infos[file.path] && infos[file.path][n]) {
582 repo.Origin = infos[file.path][n].origin || Proxmox.Utils.UnknownText;
583 repo.warnings = infos[file.path][n].warnings || [];
584 }
24313a9d
FE
585 gridData.push(repo);
586 }
587 });
588
24313a9d
FE
589 repoGrid.store.loadData(gridData);
590
591 me.updateStandardRepos(data['standard-repos']);
592 }
593
594 me.digest = digest;
595
596 vm.set('errorCount', errors.length);
597 errorGrid.store.loadData(errors);
598 });
599
600 me.check_subscription();
601 },
602
603 listeners: {
604 activate: function() {
605 let me = this;
606 me.reload();
607 },
608 },
609
610 initComponent: function() {
611 let me = this;
612
613 if (!me.nodename) {
614 throw "no node name specified";
615 }
616
617 let store = Ext.create('Ext.data.Store', {
618 proxy: {
619 type: 'proxmox',
620 url: `/api2/json/nodes/${me.nodename}/apt/repositories`,
621 },
622 });
623
624 Ext.apply(me, { store: store });
625
626 Proxmox.Utils.monStoreErrors(me, me.store, true);
627
628 me.callParent();
3fc020f4
TL
629
630 me.getViewModel().set('product', me.product);
24313a9d
FE
631 },
632});