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