]>
Commit | Line | Data |
---|---|---|
386f8d97 DC |
1 | Ext.define('PVE.tree.ResourceMapTree', { |
2 | extend: 'Ext.tree.Panel', | |
3 | alias: 'widget.pveResourceMapTree', | |
4 | mixins: ['Proxmox.Mixin.CBind'], | |
5 | ||
6 | rootVisible: false, | |
7 | ||
8 | emptyText: gettext('No Mapping found'), | |
9 | ||
10 | // will be opened on edit | |
11 | editWindowClass: undefined, | |
12 | ||
13 | // The base url of the resource | |
14 | baseUrl: undefined, | |
15 | ||
16 | // icon class to show on the entries | |
17 | mapIconCls: undefined, | |
18 | ||
19 | // if given, should be a function that takes a nodename and returns | |
20 | // the url for getting the data to check the status | |
21 | getStatusCheckUrl: undefined, | |
22 | ||
23 | // the result of above api call and the nodename is passed and can set the status | |
24 | checkValidity: undefined, | |
25 | ||
26 | // the property that denotes a single map entry for a node | |
27 | entryIdProperty: undefined, | |
28 | ||
29 | cbindData: function(initialConfig) { | |
30 | let me = this; | |
31 | const caps = Ext.state.Manager.get('GuiCap'); | |
32 | me.canConfigure = !!caps.mapping['Mapping.Modify']; | |
33 | ||
34 | return {}; | |
35 | }, | |
36 | ||
37 | controller: { | |
38 | xclass: 'Ext.app.ViewController', | |
39 | ||
40 | addMapping: function() { | |
41 | let me = this; | |
42 | let view = me.getView(); | |
43 | Ext.create(view.editWindowClass, { | |
44 | url: view.baseUrl, | |
45 | autoShow: true, | |
46 | listeners: { | |
47 | destroy: () => me.load(), | |
48 | }, | |
49 | }); | |
50 | }, | |
51 | ||
52 | addHost: function() { | |
53 | let me = this; | |
54 | me.edit(false); | |
55 | }, | |
56 | ||
57 | edit: function(includeNodename = true) { | |
58 | let me = this; | |
59 | let view = me.getView(); | |
60 | let selection = view.getSelection(); | |
61 | if (!selection || !selection.length) { | |
62 | return; | |
63 | } | |
64 | let rec = selection[0]; | |
65 | if (!view.canConfigure || (rec.data.type === 'entry' && includeNodename)) { | |
66 | return; | |
67 | } | |
68 | ||
69 | Ext.create(view.editWindowClass, { | |
70 | url: `${view.baseUrl}/${rec.data.name}`, | |
71 | autoShow: true, | |
72 | autoLoad: true, | |
73 | nodename: includeNodename ? rec.data.node : undefined, | |
74 | name: rec.data.name, | |
75 | listeners: { | |
76 | destroy: () => me.load(), | |
77 | }, | |
78 | }); | |
79 | }, | |
80 | ||
81 | remove: function() { | |
82 | let me = this; | |
83 | let view = me.getView(); | |
84 | let selection = view.getSelection(); | |
85 | if (!selection || !selection.length) { | |
86 | return; | |
87 | } | |
88 | ||
89 | let data = selection[0].data; | |
90 | let url = `${view.baseUrl}/${data.name}`; | |
91 | let method = 'PUT'; | |
92 | let params = { | |
93 | digest: me.lookup[data.name].digest, | |
94 | }; | |
95 | let map = me.lookup[data.name].map; | |
96 | switch (data.type) { | |
97 | case 'entry': | |
98 | method = 'DELETE'; | |
99 | params = undefined; | |
100 | break; | |
101 | case 'node': | |
102 | params.map = PVE.Parser.filterPropertyStringList(map, (e) => e.node !== data.node); | |
103 | break; | |
104 | case 'map': | |
105 | params.map = PVE.Parser.filterPropertyStringList(map, (e) => | |
106 | Object.entries(e).some(([key, value]) => data[key] !== value)); | |
107 | break; | |
108 | default: | |
109 | throw "invalid type"; | |
110 | } | |
111 | if (!params?.map.length) { | |
112 | method = 'DELETE'; | |
113 | params = undefined; | |
114 | } | |
115 | Proxmox.Utils.API2Request({ | |
116 | url, | |
117 | method, | |
118 | params, | |
119 | success: function() { | |
120 | me.load(); | |
121 | }, | |
122 | }); | |
123 | }, | |
124 | ||
125 | load: function() { | |
126 | let me = this; | |
127 | let view = me.getView(); | |
128 | Proxmox.Utils.API2Request({ | |
129 | url: view.baseUrl, | |
130 | method: 'GET', | |
131 | failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus), | |
132 | success: function({ result: { data } }) { | |
133 | let lookup = {}; | |
134 | data.forEach((entry) => { | |
135 | lookup[entry.id] = Ext.apply({}, entry); | |
136 | entry.iconCls = 'fa fa-fw fa-folder-o'; | |
137 | entry.name = entry.id; | |
138 | entry.text = entry.id; | |
139 | entry.type = 'entry'; | |
140 | ||
141 | let nodes = {}; | |
142 | for (const map of entry.map) { | |
143 | let parsed = PVE.Parser.parsePropertyString(map); | |
144 | parsed.iconCls = view.mapIconCls; | |
145 | parsed.leaf = true; | |
146 | parsed.name = entry.id; | |
147 | parsed.text = parsed[view.entryIdProperty]; | |
148 | parsed.type = 'map'; | |
149 | ||
150 | if (nodes[parsed.node] === undefined) { | |
151 | nodes[parsed.node] = { | |
152 | children: [], | |
153 | expanded: true, | |
154 | iconCls: 'fa fa-fw fa-building-o', | |
155 | leaf: false, | |
156 | name: entry.id, | |
157 | node: parsed.node, | |
158 | text: parsed.node, | |
159 | type: 'node', | |
160 | }; | |
161 | } | |
162 | nodes[parsed.node].children.push(parsed); | |
163 | } | |
164 | delete entry.id; | |
165 | entry.children = Object.values(nodes); | |
166 | entry.leaf = entry.children.length === 0; | |
167 | }); | |
168 | me.lookup = lookup; | |
169 | if (view.getStatusCheckUrl !== undefined && view.checkValidity !== undefined) { | |
170 | me.loadStatusData(); | |
171 | } | |
172 | view.setRootNode({ | |
173 | children: data, | |
174 | }); | |
175 | let root = view.getRootNode(); | |
176 | root.expand(); | |
177 | root.childNodes.forEach(node => node.expand()); | |
178 | }, | |
179 | }); | |
180 | }, | |
181 | ||
182 | nodeLoadingState: {}, | |
183 | ||
184 | loadStatusData: function() { | |
185 | let me = this; | |
186 | let view = me.getView(); | |
187 | PVE.data.ResourceStore.getNodes().forEach(({ node }) => { | |
188 | me.nodeLoadingState[node] = true; | |
189 | let url = view.getStatusCheckUrl(node); | |
190 | Proxmox.Utils.API2Request({ | |
191 | url, | |
192 | method: 'GET', | |
193 | failure: function(response) { | |
194 | me.nodeLoadingState[node] = false; | |
195 | view.getRootNode()?.cascade(function(rec) { | |
196 | if (rec.data.node !== node) { | |
197 | return; | |
198 | } | |
199 | ||
200 | rec.set('valid', 0); | |
201 | rec.set('errmsg', response.htmlStatus); | |
202 | rec.commit(); | |
203 | }); | |
204 | }, | |
205 | success: function({ result: { data } }) { | |
206 | me.nodeLoadingState[node] = false; | |
207 | view.checkValidity(data, node); | |
208 | }, | |
209 | }); | |
210 | }); | |
211 | }, | |
212 | ||
213 | renderStatus: function(value, _metadata, record) { | |
214 | let me = this; | |
215 | if (record.data.type !== 'map') { | |
216 | return ''; | |
217 | } | |
218 | let iconCls; | |
219 | let status; | |
220 | if (value === undefined) { | |
221 | if (me.nodeLoadingState[record.data.node]) { | |
222 | iconCls = 'fa-spinner fa-spin'; | |
223 | status = gettext('Loading...'); | |
224 | } else { | |
225 | iconCls = 'fa-question-circle'; | |
226 | status = gettext('Unknown Node'); | |
227 | } | |
228 | } else { | |
229 | let state = value ? 'good' : 'critical'; | |
230 | iconCls = PVE.Utils.get_health_icon(state, true); | |
0a571cff | 231 | status = value ? gettext("Mapping matches host data") : record.data.errmsg || Proxmox.Utils.unknownText; |
386f8d97 DC |
232 | } |
233 | return `<i class="fa ${iconCls}"></i> ${status}`; | |
234 | }, | |
235 | ||
236 | init: function(view) { | |
237 | let me = this; | |
238 | ||
239 | ['editWindowClass', 'baseUrl', 'mapIconCls', 'entryIdProperty'].forEach((property) => { | |
240 | if (view[property] === undefined) { | |
241 | throw `No ${property} defined`; | |
242 | } | |
243 | }); | |
244 | ||
245 | me.load(); | |
246 | }, | |
247 | }, | |
248 | ||
249 | store: { | |
250 | sorters: 'text', | |
251 | data: {}, | |
252 | }, | |
253 | ||
254 | ||
255 | tbar: [ | |
256 | { | |
257 | text: gettext('Add mapping'), | |
258 | handler: 'addMapping', | |
259 | cbind: { | |
260 | disabled: '{!canConfigure}', | |
261 | }, | |
262 | }, | |
263 | { | |
264 | xtype: 'proxmoxButton', | |
265 | text: gettext('New Host mapping'), | |
266 | disabled: true, | |
267 | parentXType: 'treepanel', | |
268 | enableFn: function(_rec) { | |
269 | return this.up('treepanel').canConfigure; | |
270 | }, | |
271 | handler: 'addHost', | |
272 | }, | |
273 | { | |
274 | xtype: 'proxmoxButton', | |
275 | text: gettext('Edit'), | |
276 | disabled: true, | |
277 | parentXType: 'treepanel', | |
278 | enableFn: function(rec) { | |
279 | return rec && rec.data.type !== 'entry' && this.up('treepanel').canConfigure; | |
280 | }, | |
281 | handler: 'edit', | |
282 | }, | |
283 | { | |
284 | xtype: 'proxmoxButton', | |
285 | parentXType: 'treepanel', | |
286 | handler: 'remove', | |
287 | disabled: true, | |
288 | text: gettext('Remove'), | |
289 | enableFn: function(rec) { | |
290 | return rec && this.up('treepanel').canConfigure; | |
291 | }, | |
292 | confirmMsg: function(rec) { | |
293 | let msg, id; | |
294 | let view = this.up('treepanel'); | |
295 | switch (rec.data.type) { | |
296 | case 'entry': | |
297 | msg = gettext("Are you sure you want to remove '{0}'"); | |
298 | return Ext.String.format(msg, rec.data.name); | |
299 | case 'node': | |
300 | msg = gettext("Are you sure you want to remove '{0}' entries for '{1}'"); | |
301 | return Ext.String.format(msg, rec.data.node, rec.data.name); | |
302 | case 'map': | |
303 | msg = gettext("Are you sure you want to remove '{0}' on '{1}' for '{2}'"); | |
304 | id = rec.data[view.entryIdProperty]; | |
305 | return Ext.String.format(msg, id, rec.data.node, rec.data.name); | |
306 | default: | |
307 | throw "invalid type"; | |
308 | } | |
309 | }, | |
310 | }, | |
311 | ], | |
312 | ||
313 | listeners: { | |
314 | itemdblclick: 'edit', | |
315 | }, | |
316 | }); |