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