]> git.proxmox.com Git - pve-manager.git/blame - www/manager6/tree/ResourceMapTree.js
ui: resource map tree: make 'ok' status clearer
[pve-manager.git] / www / manager6 / tree / ResourceMapTree.js
CommitLineData
386f8d97
DC
1Ext.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});