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