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