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