]> git.proxmox.com Git - pve-manager.git/blob - www/manager6/ceph/Status.js
ui: ceph warnings: disable copy-all if there are no additional infos
[pve-manager.git] / www / manager6 / ceph / Status.js
1 Ext.define('pve-ceph-warnings', {
2 extend: 'Ext.data.Model',
3 fields: ['id', 'summary', 'detail', 'severity'],
4 idProperty: 'id',
5 });
6
7
8 Ext.define('PVE.node.CephStatus', {
9 extend: 'Ext.panel.Panel',
10 alias: 'widget.pveNodeCephStatus',
11
12 onlineHelp: 'chapter_pveceph',
13
14 scrollable: true,
15 bodyPadding: 5,
16 layout: {
17 type: 'column',
18 },
19
20 defaults: {
21 padding: 5,
22 },
23
24 items: [
25 {
26 xtype: 'panel',
27 title: gettext('Health'),
28 bodyPadding: 10,
29 plugins: 'responsive',
30 responsiveConfig: {
31 'width < 1600': {
32 minHeight: 230,
33 columnWidth: 1,
34 },
35 'width >= 1600': {
36 minHeight: 500,
37 columnWidth: 0.5,
38 },
39 },
40 layout: {
41 type: 'hbox',
42 align: 'stretch',
43 },
44 items: [
45 {
46 xtype: 'container',
47 layout: {
48 type: 'vbox',
49 align: 'stretch',
50 },
51 flex: 1,
52 items: [
53 {
54
55 xtype: 'pveHealthWidget',
56 itemId: 'overallhealth',
57 flex: 1,
58 title: gettext('Status'),
59 },
60 {
61 xtype: 'displayfield',
62 itemId: 'versioninfo',
63 fieldLabel: gettext('Ceph Version'),
64 value: "",
65 autoEl: {
66 tag: 'div',
67 'data-qtip': gettext('The newest version installed in the Cluster.'),
68 },
69 padding: '10 0 0 0',
70 style: {
71 'text-align': 'center',
72 },
73 },
74 ],
75 },
76 {
77 xtype: 'grid',
78 itemId: 'warnings',
79 flex: 2,
80 maxHeight: 430,
81 stateful: true,
82 stateId: 'ceph-status-warnings',
83 viewConfig: {
84 enableTextSelection: true,
85 },
86 // we load the store manually, to show an emptyText specify an empty intermediate store
87 store: {
88 type: 'diff',
89 trackRemoved: false,
90 data: [],
91 rstore: {
92 storeid: 'pve-ceph-warnings',
93 type: 'update',
94 model: 'pve-ceph-warnings',
95 },
96 },
97 updateHealth: function(health) {
98 let checks = health.checks || {};
99
100 let checkRecords = Object.keys(checks).sort().map(key => {
101 let check = checks[key];
102 let data = {
103 id: key,
104 summary: check.summary.message,
105 detail: check.detail.reduce((acc, v) => `${acc}\n${v.message}`, '').trimStart(),
106 severity: check.severity,
107 };
108 data.noDetails = data.detail.length === 0;
109 if (data.detail.length === 0) {
110 data.detail = "no additional data";
111 }
112 return data;
113 });
114
115 let rstore = this.getStore().rstore;
116 rstore.loadData(checkRecords, false);
117 rstore.fireEvent('load', rstore, checkRecords, true);
118 },
119 emptyText: gettext('No Warnings/Errors'),
120 columns: [
121 {
122 dataIndex: 'severity',
123 tooltip: gettext('Severity'),
124 align: 'center',
125 width: 38,
126 renderer: function(value) {
127 let health = PVE.Utils.map_ceph_health[value];
128 let icon = PVE.Utils.get_health_icon(health);
129 return `<i class="fa fa-fw ${icon}"></i>`;
130 },
131 sorter: {
132 sorterFn: function(a, b) {
133 let health = ['HEALTH_ERR', 'HEALTH_WARN', 'HEALTH_OK'];
134 return health.indexOf(b.data.severity) - health.indexOf(a.data.severity);
135 },
136 },
137 },
138 {
139 dataIndex: 'summary',
140 header: gettext('Summary'),
141 flex: 1,
142 },
143 {
144 xtype: 'actioncolumn',
145 width: 50,
146 align: 'center',
147 tooltip: gettext('Actions'),
148 items: [
149 {
150 iconCls: 'x-fa fa-files-o',
151 tooltip: gettext('Copy Summary'),
152 handler: function(grid, rowindex, colindex, item, e, record) {
153 navigator.clipboard
154 .writeText(record.data.summary)
155 .catch(err => Ext.Msg.alert(gettext('Error'), err));
156 },
157 },
158 {
159 iconCls: 'x-fa fa-clipboard',
160 tooltip: gettext('Copy All'),
161 isActionDisabled: (v, r, c, i, { data }) => !!data.noDetails,
162 handler: function(grid, rowindex, colindex, item, e, { data }) {
163 navigator.clipboard
164 .writeText(`${data.severity}: ${data.summary}\n${data.detail}`)
165 .catch(err => Ext.Msg.alert(gettext('Error'), err));
166 },
167 },
168 ],
169 },
170 ],
171 listeners: {
172 itemdblclick: function(view, record, row, rowIdx, e) {
173 // inspired by Ext.grid.plugin.RowExpander, but for double click
174 let rowNode = view.getNode(rowIdx);
175 let normalRow = Ext.fly(rowNode);
176
177 let collapsedCls = view.rowBodyFeature.rowCollapsedCls;
178
179 if (normalRow.hasCls(collapsedCls)) {
180 view.rowBodyFeature.rowExpander.toggleRow(rowIdx, record);
181 }
182 },
183 },
184 plugins: [
185 {
186 ptype: 'rowexpander',
187 expandOnDblClick: false,
188 rowBodyTpl: '<pre class="pve-ceph-warning-detail">{detail}</pre>',
189 },
190 ],
191 },
192 ],
193 },
194 {
195 xtype: 'pveCephStatusDetail',
196 itemId: 'statusdetail',
197 plugins: 'responsive',
198 responsiveConfig: {
199 'width < 1600': {
200 columnWidth: 1,
201 minHeight: 250,
202 },
203 'width >= 1600': {
204 columnWidth: 0.5,
205 minHeight: 300,
206 },
207 },
208 title: gettext('Status'),
209 },
210 {
211 xtype: 'pveCephServices',
212 title: gettext('Services'),
213 itemId: 'services',
214 plugins: 'responsive',
215 layout: {
216 type: 'hbox',
217 align: 'stretch',
218 },
219 responsiveConfig: {
220 'width < 1600': {
221 columnWidth: 1,
222 minHeight: 200,
223 },
224 'width >= 1600': {
225 columnWidth: 0.5,
226 minHeight: 200,
227 },
228 },
229 },
230 {
231 xtype: 'panel',
232 title: gettext('Performance'),
233 columnWidth: 1,
234 bodyPadding: 5,
235 layout: {
236 type: 'hbox',
237 align: 'center',
238 },
239 items: [
240 {
241 xtype: 'container',
242 flex: 1,
243 items: [
244 {
245 xtype: 'proxmoxGauge',
246 itemId: 'space',
247 title: gettext('Usage'),
248 },
249 {
250 flex: 1,
251 border: false,
252 },
253 {
254 xtype: 'container',
255 itemId: 'recovery',
256 hidden: true,
257 padding: 25,
258 items: [
259 {
260 xtype: 'pveRunningChart',
261 itemId: 'recoverychart',
262 title: gettext('Recovery') +'/ '+ gettext('Rebalance'),
263 renderer: PVE.Utils.render_bandwidth,
264 height: 100,
265 },
266 {
267 xtype: 'progressbar',
268 itemId: 'recoveryprogress',
269 },
270 ],
271 },
272 ],
273 },
274 {
275 xtype: 'container',
276 flex: 2,
277 defaults: {
278 padding: 0,
279 height: 100,
280 },
281 items: [
282 {
283 xtype: 'pveRunningChart',
284 itemId: 'reads',
285 title: gettext('Reads'),
286 renderer: PVE.Utils.render_bandwidth,
287 },
288 {
289 xtype: 'pveRunningChart',
290 itemId: 'writes',
291 title: gettext('Writes'),
292 renderer: PVE.Utils.render_bandwidth,
293 },
294 {
295 xtype: 'pveRunningChart',
296 itemId: 'readiops',
297 title: 'IOPS: ' + gettext('Reads'),
298 renderer: Ext.util.Format.numberRenderer('0,000'),
299 },
300 {
301 xtype: 'pveRunningChart',
302 itemId: 'writeiops',
303 title: 'IOPS: ' + gettext('Writes'),
304 renderer: Ext.util.Format.numberRenderer('0,000'),
305 },
306 ],
307 },
308 ],
309 },
310 ],
311
312 updateAll: function(store, records, success) {
313 if (!success || records.length === 0) {
314 return;
315 }
316
317 var me = this;
318 var rec = records[0];
319 me.status = rec.data;
320
321 // add health panel
322 me.down('#overallhealth').updateHealth(PVE.Utils.render_ceph_health(rec.data.health || {}));
323 me.down('#warnings').updateHealth(rec.data.health || {}); // add errors to gridstore
324
325 me.getComponent('services').updateAll(me.metadata || {}, rec.data);
326
327 me.getComponent('statusdetail').updateAll(me.metadata || {}, rec.data);
328
329 // add performance data
330 let pgmap = rec.data.pgmap;
331 let used = pgmap.bytes_used;
332 let total = pgmap.bytes_total;
333
334 var text = Ext.String.format(gettext('{0} of {1}'),
335 Proxmox.Utils.render_size(used),
336 Proxmox.Utils.render_size(total),
337 );
338
339 // update the usage widget
340 me.down('#space').updateValue(used/total, text);
341
342 let readiops = pgmap.read_op_per_sec;
343 let writeiops = pgmap.write_op_per_sec;
344 let reads = pgmap.read_bytes_sec || 0;
345 let writes = pgmap.write_bytes_sec || 0;
346
347 // update the graphs
348 me.reads.addDataPoint(reads);
349 me.writes.addDataPoint(writes);
350 me.readiops.addDataPoint(readiops);
351 me.writeiops.addDataPoint(writeiops);
352
353 let degraded = pgmap.degraded_objects || 0;
354 let misplaced = pgmap.misplaced_objects || 0;
355 let unfound = pgmap.unfound_objects || 0;
356 let unhealthy = degraded + unfound + misplaced;
357 // update recovery
358 if (pgmap.recovering_objects_per_sec !== undefined || unhealthy > 0) {
359 let toRecoverObjects = pgmap.misplaced_total || pgmap.unfound_total || pgmap.degraded_total || 0;
360 if (toRecoverObjects === 0) {
361 return; // FIXME: unexpected return and leaves things possible visible when it shouldn't?
362 }
363 let recovered = toRecoverObjects - unhealthy || 0;
364 let speed = pgmap.recovering_bytes_per_sec || 0;
365
366 let recoveryRatio = recovered / toRecoverObjects;
367 let txt = `${(recoveryRatio * 100).toFixed(2)}%`;
368 if (speed > 0) {
369 let obj_per_sec = speed / (4 * 1024 * 1024); // 4 MiB per Object
370 let duration = Proxmox.Utils.format_duration_human(unhealthy/obj_per_sec);
371 let speedTxt = PVE.Utils.render_bandwidth(speed);
372 txt += ` (${speedTxt} - ${duration} left)`;
373 }
374
375 me.down('#recovery').setVisible(true);
376 me.down('#recoveryprogress').updateValue(recoveryRatio);
377 me.down('#recoveryprogress').updateText(txt);
378 me.down('#recoverychart').addDataPoint(speed);
379 } else {
380 me.down('#recovery').setVisible(false);
381 me.down('#recoverychart').addDataPoint(0);
382 }
383 },
384
385 initComponent: function() {
386 var me = this;
387
388 var nodename = me.pveSelNode.data.node;
389
390 me.callParent();
391 var baseurl = '/api2/json' + (nodename ? '/nodes/' + nodename : '/cluster') + '/ceph';
392 me.store = Ext.create('Proxmox.data.UpdateStore', {
393 storeid: 'ceph-status-' + (nodename || 'cluster'),
394 interval: 5000,
395 proxy: {
396 type: 'proxmox',
397 url: baseurl + '/status',
398 },
399 });
400
401 me.metadatastore = Ext.create('Proxmox.data.UpdateStore', {
402 storeid: 'ceph-metadata-' + (nodename || 'cluster'),
403 interval: 15*1000,
404 proxy: {
405 type: 'proxmox',
406 url: '/api2/json/cluster/ceph/metadata',
407 },
408 });
409
410 // save references for the updatefunction
411 me.iops = me.down('#iops');
412 me.readiops = me.down('#readiops');
413 me.writeiops = me.down('#writeiops');
414 me.reads = me.down('#reads');
415 me.writes = me.down('#writes');
416
417 // manages the "install ceph?" overlay
418 PVE.Utils.monitor_ceph_installed(me, me.store, nodename);
419
420 me.mon(me.store, 'load', me.updateAll, me);
421 me.mon(me.metadatastore, 'load', function(store, records, success) {
422 if (!success || records.length < 1) {
423 return;
424 }
425 me.metadata = records[0].data;
426
427 // update services
428 me.getComponent('services').updateAll(me.metadata, me.status || {});
429
430 // update detailstatus panel
431 me.getComponent('statusdetail').updateAll(me.metadata, me.status || {});
432
433 let maxversion = [];
434 let maxversiontext = "";
435 for (const [_nodename, data] of Object.entries(me.metadata.node)) {
436 let version = data.version.parts;
437 if (PVE.Utils.compare_ceph_versions(version, maxversion) > 0) {
438 maxversion = version;
439 maxversiontext = data.version.str;
440 }
441 }
442 me.down('#versioninfo').setValue(maxversiontext);
443 }, me);
444
445 me.on('destroy', me.store.stopUpdate);
446 me.on('destroy', me.metadatastore.stopUpdate);
447 me.store.startUpdate();
448 me.metadatastore.startUpdate();
449 },
450
451 });