]> git.proxmox.com Git - proxmox-widget-toolkit.git/blob - src/node/APTRepositories.js
node: repos: increase width of add-repo window + style cleanup
[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 width: 600,
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
90 Ext.apply(me, {
91 items: [
92 repoSelector,
93 description,
94 status,
95 ],
96 repoSelector: repoSelector,
97 });
98
99 me.callParent();
100 },
101 });
102
103 Ext.define('Proxmox.node.APTRepositoriesErrors', {
104 extend: 'Ext.grid.GridPanel',
105
106 xtype: 'proxmoxNodeAPTRepositoriesErrors',
107
108 title: gettext('Errors'),
109
110 store: {},
111
112 border: false,
113
114 viewConfig: {
115 stripeRows: false,
116 getRowClass: () => 'proxmox-invalid-row',
117 },
118
119 columns: [
120 {
121 header: gettext('File'),
122 dataIndex: 'path',
123 renderer: value => `<i class='pve-grid-fa fa fa-fw fa-exclamation-triangle'></i>${value}`,
124 width: 350,
125 },
126 {
127 header: gettext('Error'),
128 dataIndex: 'error',
129 flex: 1,
130 },
131 ],
132 });
133
134 Ext.define('Proxmox.node.APTRepositoriesGrid', {
135 extend: 'Ext.grid.GridPanel',
136 xtype: 'proxmoxNodeAPTRepositoriesGrid',
137
138 title: gettext('APT Repositories'),
139
140 cls: 'proxmox-apt-repos', // to allow applying styling to general components with local effect
141
142 border: false,
143
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 },
153 {
154 text: gettext('Add'),
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 });
180 },
181 },
182 '-',
183 {
184 xtype: 'proxmoxButton',
185 text: gettext('Enable'),
186 defaultText: gettext('Enable'),
187 altText: gettext('Disable'),
188 id: 'repoEnableButton',
189 disabled: true,
190 bind: {
191 text: '{enableButtonText}',
192 },
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 },
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 },
232 },
233 ],
234
235 sortableColumns: false,
236
237 columns: [
238 {
239 xtype: 'checkcolumn',
240 header: gettext('Enabled'),
241 dataIndex: 'Enabled',
242 listeners: {
243 beforecheckchange: () => false, // veto, we don't want to allow inline change - to subtle
244 },
245 width: 90,
246 },
247 {
248 header: gettext('Types'),
249 dataIndex: 'Types',
250 renderer: function(types, cell, record) {
251 return types.join(' ');
252 },
253 width: 100,
254 },
255 {
256 header: gettext('URIs'),
257 dataIndex: 'URIs',
258 renderer: function(uris, cell, record) {
259 return uris.join(' ');
260 },
261 width: 350,
262 },
263 {
264 header: gettext('Suites'),
265 dataIndex: 'Suites',
266 renderer: function(suites, metaData, record) {
267 let err = '';
268 if (record.data.warnings && record.data.warnings.length > 0) {
269 let txt = [gettext('Warning')];
270 record.data.warnings.forEach((warning) => {
271 if (warning.property === 'Suites') {
272 txt.push(warning.message);
273 }
274 });
275 metaData.tdAttr = `data-qtip="${Ext.htmlEncode(txt.join('<br>'))}"`;
276 metaData.tdCls = 'proxmox-invalid-row';
277 err = '<i class="fa fa-fw critical fa-exclamation-circle"></i> ';
278 }
279 return suites.join(' ') + err;
280 },
281 width: 130,
282 },
283 {
284 header: gettext('Components'),
285 dataIndex: 'Components',
286 renderer: function(components, cell, record) {
287 return components.join(' ');
288 },
289 width: 170,
290 },
291 {
292 header: gettext('Options'),
293 dataIndex: 'Options',
294 renderer: function(options, cell, record) {
295 if (!options) {
296 return '';
297 }
298
299 let filetype = record.data.FileType;
300 let text = '';
301
302 options.forEach(function(option) {
303 let key = option.Key;
304 if (filetype === 'list') {
305 let values = option.Values.join(',');
306 text += `${key}=${values} `;
307 } else if (filetype === 'sources') {
308 let values = option.Values.join(' ');
309 text += `${key}: ${values}<br>`;
310 } else {
311 throw "unkown file type";
312 }
313 });
314 return text;
315 },
316 flex: 1,
317 },
318 {
319 header: gettext('Origin'),
320 dataIndex: 'Origin',
321 width: 120,
322 renderer: (value, meta, rec) => {
323 let cls = 'fa fa-fw fa-question-circle-o';
324 if (value.match(/^\s*Proxmox\s*$/i)) {
325 cls = 'pmx-itype-icon pmx-itype-icon-proxmox-x';
326 } else if (value.match(/^\s*Debian\s*$/i)) {
327 cls = 'pmx-itype-icon pmx-itype-icon-debian-swirl';
328 }
329 return `<i class='${cls}'></i> ${value}`;
330 },
331 },
332 {
333 header: gettext('Comment'),
334 dataIndex: 'Comment',
335 flex: 2,
336 },
337 ],
338
339 initComponent: function() {
340 let me = this;
341
342 if (!me.nodename) {
343 throw "no node name specified";
344 }
345
346 let store = Ext.create('Ext.data.Store', {
347 model: 'apt-repolist',
348 groupField: 'Path',
349 sorters: [
350 {
351 property: 'Index',
352 direction: 'ASC',
353 },
354 ],
355 });
356
357 let groupingFeature = Ext.create('Ext.grid.feature.Grouping', {
358 groupHeaderTpl: '{[ "File: " + values.name ]} ({rows.length} ' +
359 'repositor{[values.rows.length > 1 ? "ies" : "y"]})',
360 enableGroupingMenu: false,
361 });
362
363 Ext.apply(me, {
364 store: store,
365 features: [groupingFeature],
366 });
367
368 me.callParent();
369 },
370 });
371
372 Ext.define('Proxmox.node.APTRepositories', {
373 extend: 'Ext.panel.Panel',
374 xtype: 'proxmoxNodeAPTRepositories',
375 mixins: ['Proxmox.Mixin.CBind'],
376
377 digest: undefined,
378
379 product: 'Proxmox VE', // default
380
381 controller: {
382 xclass: 'Ext.app.ViewController',
383
384 selectionChange: function(grid, selection) {
385 let me = this;
386 if (!selection || selection.length < 1) {
387 return;
388 }
389 let rec = selection[0];
390 let vm = me.getViewModel();
391 vm.set('selectionenabled', rec.get('Enabled'));
392 },
393 },
394
395 viewModel: {
396 data: {
397 product: 'Proxmox VE', // default
398 errorCount: 0,
399 subscriptionActive: '',
400 noSubscriptionRepo: '',
401 enterpriseRepo: '',
402 selectionenabled: false,
403 },
404 formulas: {
405 noErrors: (get) => get('errorCount') === 0,
406 enableButtonText: (get) => get('selectionenabled')
407 ? gettext('Disable') : gettext('Enable'),
408 mainWarning: function(get) {
409 // Not yet initialized
410 if (get('subscriptionActive') === '' ||
411 get('enterpriseRepo') === '') {
412 return '';
413 }
414
415 let icon = `<i class='fa fa-fw fa-exclamation-triangle critical'></i>`;
416 let fmt = (msg) => `<div class="black">${icon}${gettext('Warning')}: ${msg}</div>`;
417
418 if (!get('subscriptionActive') && get('enterpriseRepo')) {
419 return fmt(gettext('The enterprise repository is enabled, but there is no active subscription!'));
420 }
421
422 if (get('noSubscriptionRepo')) {
423 return fmt(gettext('The no-subscription repository is not recommended for production use!'));
424 }
425
426 if (!get('enterpriseRepo') && !get('noSubscriptionRepo')) {
427 let msg = Ext.String.format(gettext('No {0} repository is enabled!'), get('product'));
428 return fmt(msg);
429 }
430
431 return '';
432 },
433 },
434 },
435
436 scrollable: true,
437 layout: {
438 type: 'vbox',
439 align: 'stretch',
440 },
441
442 items: [
443 {
444 xtype: 'header',
445 baseCls: 'x-panel-header',
446 bind: {
447 hidden: '{!mainWarning}',
448 title: '{mainWarning}',
449 },
450 },
451 {
452 xtype: 'box',
453 bind: {
454 hidden: '{!mainWarning}',
455 },
456 height: 5,
457 },
458 {
459 xtype: 'proxmoxNodeAPTRepositoriesErrors',
460 name: 'repositoriesErrors',
461 hidden: true,
462 padding: '0 0 5 0',
463 bind: {
464 hidden: '{noErrors}',
465 },
466 },
467 {
468 xtype: 'proxmoxNodeAPTRepositoriesGrid',
469 name: 'repositoriesGrid',
470 cbind: {
471 nodename: '{nodename}',
472 },
473 majorUpgradeAllowed: false, // TODO get release information from an API call?
474 listeners: {
475 selectionchange: 'selectionChange',
476 },
477 },
478 ],
479
480 check_subscription: function() {
481 let me = this;
482 let vm = me.getViewModel();
483
484 Proxmox.Utils.API2Request({
485 url: `/nodes/${me.nodename}/subscription`,
486 method: 'GET',
487 failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
488 success: function(response, opts) {
489 const res = response.result;
490 const subscription = !(!res || !res.data || res.data.status.toLowerCase() !== 'active');
491 vm.set('subscriptionActive', subscription);
492 },
493 });
494 },
495
496 updateStandardRepos: function(standardRepos) {
497 let me = this;
498 let vm = me.getViewModel();
499
500 let addButton = me.down('#addButton');
501 addButton.repoInfo = [];
502
503 for (const standardRepo of standardRepos) {
504 const handle = standardRepo.handle;
505 const status = standardRepo.status;
506
507 if (handle === "enterprise") {
508 vm.set('enterpriseRepo', status);
509 } else if (handle === "no-subscription") {
510 vm.set('noSubscriptionRepo', status);
511 }
512
513 addButton.repoInfo.push(standardRepo);
514 addButton.digest = me.digest;
515 }
516
517 addButton.setDisabled(false);
518 },
519
520 reload: function() {
521 let me = this;
522 let vm = me.getViewModel();
523 let repoGrid = me.down('proxmoxNodeAPTRepositoriesGrid');
524 let errorGrid = me.down('proxmoxNodeAPTRepositoriesErrors');
525
526 me.store.load(function(records, operation, success) {
527 let gridData = [];
528 let errors = [];
529 let digest;
530
531 if (success && records.length > 0) {
532 let data = records[0].data;
533 let files = data.files;
534 errors = data.errors;
535 digest = data.digest;
536
537 let infos = {};
538 for (const info of data.infos) {
539 let path = info.path;
540 let idx = info.index;
541
542 if (!infos[path]) {
543 infos[path] = {};
544 }
545 if (!infos[path][idx]) {
546 infos[path][idx] = {
547 origin: '',
548 warnings: [],
549 };
550 }
551
552 if (info.kind === 'origin') {
553 infos[path][idx].origin = info.message;
554 } else if (info.kind === 'warning' ||
555 (info.kind === 'ignore-pre-upgrade-warning' && !repoGrid.majorUpgradeAllowed)
556 ) {
557 infos[path][idx].warnings.push(info);
558 } else {
559 throw 'unknown info';
560 }
561 }
562
563
564 files.forEach(function(file) {
565 for (let n = 0; n < file.repositories.length; n++) {
566 let repo = file.repositories[n];
567 repo.Path = file.path;
568 repo.Index = n;
569 if (infos[file.path] && infos[file.path][n]) {
570 repo.Origin = infos[file.path][n].origin || Proxmox.Utils.UnknownText;
571 repo.warnings = infos[file.path][n].warnings || [];
572 }
573 gridData.push(repo);
574 }
575 });
576
577 repoGrid.store.loadData(gridData);
578
579 me.updateStandardRepos(data['standard-repos']);
580 }
581
582 me.digest = digest;
583
584 vm.set('errorCount', errors.length);
585 errorGrid.store.loadData(errors);
586 });
587
588 me.check_subscription();
589 },
590
591 listeners: {
592 activate: function() {
593 let me = this;
594 me.reload();
595 },
596 },
597
598 initComponent: function() {
599 let me = this;
600
601 if (!me.nodename) {
602 throw "no node name specified";
603 }
604
605 let store = Ext.create('Ext.data.Store', {
606 proxy: {
607 type: 'proxmox',
608 url: `/api2/json/nodes/${me.nodename}/apt/repositories`,
609 },
610 });
611
612 Ext.apply(me, { store: store });
613
614 Proxmox.Utils.monStoreErrors(me, me.store, true);
615
616 me.callParent();
617
618 me.getViewModel().set('product', me.product);
619 },
620 });