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