Skip to content

Commit 4bfe801

Browse files
committed
feat: event usage metric view
1 parent 3d34cd7 commit 4bfe801

File tree

14 files changed

+562
-2
lines changed

14 files changed

+562
-2
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,6 @@ coverage
1212

1313
# Mac files
1414
**/.DS_Store
15-
**/__MACOSX
15+
**/__MACOSX
16+
17+
/sampleData.js

src/main/default/classes/StreamingMonitorController.cls

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,15 @@ public abstract class StreamingMonitorController {
7676
return output;
7777
}
7878

79+
@AuraEnabled
80+
public static List<PlatformEventUsageMetric> getEventUsageMetrics() {
81+
return [
82+
SELECT Name, StartDate, Value
83+
FROM PlatformEventUsageMetric
84+
ORDER BY StartDate
85+
];
86+
}
87+
7988
private static void publishPlatformEvent(
8089
String eventName,
8190
String eventPayload
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
c-event-usage-metrics-filters {
2+
display: block;
3+
margin: 0 auto;
4+
padding-bottom: 1rem;
5+
}
6+
7+
circle:hover {
8+
stroke: black;
9+
stroke-width: 1px;
10+
}
11+
12+
.tooltip {
13+
position: fixed;
14+
text-align: center;
15+
padding: 4px;
16+
background: black;
17+
color: white;
18+
pointer-events: none;
19+
z-index: 1000;
20+
}
21+
22+
.tooltip.data {
23+
background: #005583;
24+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<template>
2+
<lightning-card title="Event usage metrics">
3+
<div slot="actions">
4+
<lightning-badge label={eventCountLabel}></lightning-badge>
5+
</div>
6+
7+
<div class="slds-p-horizontal_large">
8+
<c-notice
9+
>Each dot represents an hour. The time that is displayed is the
10+
start of the period.</c-notice
11+
>
12+
<div class="slds-m-bottom_large slds-border_bottom">
13+
<c-event-usage-metrics-filters
14+
event-types={eventTypes}
15+
onfilterchange={handleFilterChange}
16+
></c-event-usage-metrics-filters>
17+
</div>
18+
<div class={timelineClasses} lwc:dom="manual"></div>
19+
</div>
20+
</lightning-card>
21+
</template>
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
/* global d3 */
2+
import { LightningElement } from 'lwc';
3+
import { loadScript } from 'lightning/platformResourceLoader';
4+
import D3 from '@salesforce/resourceUrl/d3';
5+
import getEventUsageMetrics from '@salesforce/apex/StreamingMonitorController.getEventUsageMetrics';
6+
import { getTimeLabel, toTitleCase } from 'c/streamingUtility';
7+
8+
export default class EventUsageMetrics extends LightningElement {
9+
allMetrics;
10+
filteredMetrics;
11+
eventTypes = [];
12+
13+
isD3Initialized = false;
14+
dimensions;
15+
bounds;
16+
tooltipElement;
17+
isDataTooltip = false;
18+
xScale;
19+
yScale;
20+
xAxis;
21+
yAxis;
22+
23+
async connectedCallback() {
24+
try {
25+
let [metrics] = await Promise.all([
26+
getEventUsageMetrics(),
27+
loadScript(this, D3)
28+
]);
29+
this.isD3Initialized = true;
30+
const eventNames = Array.from(
31+
new Set(metrics.map((m) => m.Name))
32+
).sort();
33+
const eventIndexes = new Map();
34+
eventNames.forEach((item, index) => eventIndexes.set(item, index));
35+
metrics = metrics.map((metric) => {
36+
const time = new Date(metric.StartDate);
37+
return {
38+
type: eventIndexes.get(metric.Name),
39+
timestamp: time.getTime(),
40+
timeLabel: getTimeLabel(time),
41+
value: metric.Value
42+
};
43+
});
44+
const colorScale = d3
45+
.scaleSequential()
46+
.domain([0, eventNames.length])
47+
.interpolator(d3.interpolateRainbow);
48+
this.allMetrics = this.filteredMetrics = metrics;
49+
this.eventTypes = eventNames.map((type, index) => ({
50+
index,
51+
label: toTitleCase(type),
52+
color: colorScale(index)
53+
}));
54+
55+
this.initializeTimeline();
56+
} catch (error) {
57+
console.error('Failed to initialize chart', JSON.stringify(error));
58+
}
59+
}
60+
61+
async renderedCallback() {
62+
this.initializeTimeline();
63+
}
64+
65+
initializeTimeline() {
66+
if (!this.isD3Initialized) {
67+
return;
68+
}
69+
70+
const rootElement = this.template.querySelector('.timeline');
71+
rootElement.childNodes.forEach((childNode) => childNode.remove());
72+
73+
// Add SVG element
74+
const svgElement = d3
75+
.select(rootElement)
76+
.append('svg')
77+
.attr('width', '100%')
78+
.attr('height', '400px');
79+
80+
// Get chart dimensions & use them as the SVG viewbox
81+
const rootElementRect = rootElement.getBoundingClientRect();
82+
this.dimensions = {
83+
x: rootElementRect.x,
84+
y: rootElementRect.y,
85+
width: rootElementRect.width,
86+
height: rootElementRect.height,
87+
margin: { top: 40, right: 40, bottom: 40, left: 40 }
88+
};
89+
svgElement
90+
.attr(
91+
'viewBox',
92+
`0 0 ${this.dimensions.width} ${this.dimensions.height}`
93+
)
94+
.on('mousemove', (event) => {
95+
if (this.isDataTooltip) {
96+
return;
97+
}
98+
99+
const mousePos = d3.pointer(event);
100+
if (
101+
mousePos[0] > this.dimensions.margin.left &&
102+
mousePos[1] <
103+
this.dimensions.height - this.dimensions.margin.bottom
104+
) {
105+
const time = this.xScale.invert(mousePos[0]);
106+
const timeLabel = getTimeLabel(time);
107+
this.drawTooltip(mousePos, timeLabel);
108+
} else {
109+
this.hideTooltip();
110+
}
111+
})
112+
.on('mouseout', () => {
113+
this.hideTooltip();
114+
});
115+
this.bounds = svgElement.append('g');
116+
117+
// Add axis wrappers
118+
this.bounds
119+
.append('g')
120+
.attr('id', 'x-axis')
121+
.attr(
122+
'transform',
123+
`translate(0, ${
124+
this.dimensions.height - this.dimensions.margin.bottom
125+
})`
126+
);
127+
this.bounds
128+
.append('g')
129+
.attr('id', 'y-axis')
130+
.attr('transform', `translate(${this.dimensions.margin.left}, 0)`);
131+
132+
// Init scales
133+
this.xScale = d3
134+
.scaleTime()
135+
.range([
136+
this.dimensions.margin.left,
137+
this.dimensions.width - this.dimensions.margin.right
138+
]);
139+
this.yScale = d3
140+
.scaleLog()
141+
.range([
142+
this.dimensions.height - this.dimensions.margin.bottom,
143+
this.dimensions.margin.top
144+
]);
145+
146+
// Init axes
147+
this.xAxis = d3.axisBottom(this.xScale);
148+
this.yAxis = d3.axisLeft(this.yScale);
149+
150+
// Add tooltip element
151+
this.tooltipElement = d3
152+
.select(rootElement)
153+
.append('div')
154+
.style('visibility', 'hidden');
155+
156+
// Draw timeline
157+
try {
158+
this.drawTimeline();
159+
} catch (error) {
160+
console.error('Failed to draw chart', error);
161+
}
162+
}
163+
164+
drawTimeline() {
165+
// Get x (time) range
166+
const xMin = this.filteredMetrics[0].timestamp;
167+
const xMax =
168+
this.filteredMetrics[this.filteredMetrics.length - 1].timestamp;
169+
const chartTimeMargin = (xMax - xMin) * 0.1; // 10% time margin on start and end
170+
// Get y values
171+
const values = this.filteredMetrics.map((m) => m.value);
172+
173+
// Update scales
174+
this.xScale.domain([xMin - chartTimeMargin, xMax + chartTimeMargin]);
175+
this.yScale.domain([Math.min(...values), Math.max(...values)]);
176+
177+
// Update axes
178+
const xAxisElement = this.bounds.select('#x-axis');
179+
xAxisElement.selectAll('g').remove();
180+
xAxisElement.call(this.xAxis);
181+
this.bounds.select('#y-axis').transition().call(this.yAxis);
182+
183+
// Draw data points
184+
const circles = this.bounds.selectAll('circle');
185+
circles
186+
.data(this.filteredMetrics)
187+
.join('circle')
188+
.attr('cx', (d) => this.xScale(d.timestamp))
189+
.attr('cy', (d) => this.yScale(d.value))
190+
.attr('r', 10)
191+
.attr('stroke', 'white')
192+
.style('fill', (d) => this.eventTypes[d.type].color)
193+
.on('mouseenter', (event, d) => {
194+
this.isDataTooltip = true;
195+
const mousePos = d3.pointer(event);
196+
const label = `${d.timeLabel}<br/>${d.value} ${
197+
this.eventTypes[d.type].label
198+
}`;
199+
this.drawTooltip(mousePos, label);
200+
})
201+
.on('mouseout', () => {
202+
this.isDataTooltip = false;
203+
});
204+
}
205+
206+
hideTooltip() {
207+
this.tooltipElement.style('visibility', 'hidden');
208+
}
209+
210+
drawTooltip(mousePos, label) {
211+
// Update tooltip content in order to recalculate size
212+
this.tooltipElement.html(label);
213+
const tooltipRect = this.tooltipElement.node().getBoundingClientRect();
214+
// Calculate tooltip pos
215+
const posX = mousePos[0] + this.dimensions.x - tooltipRect.width / 2;
216+
const posY = mousePos[1] + this.dimensions.y - 30 - tooltipRect.height;
217+
// Set tooltip pos and display it
218+
this.tooltipElement
219+
.style('left', `${posX}px`)
220+
.style('top', `${posY}px`)
221+
.style('visibility', 'visible')
222+
.attr('class', this.isDataTooltip ? 'tooltip data' : 'tooltip');
223+
}
224+
225+
handleFilterChange(event) {
226+
const { afterTime, beforeTime, eventTypes } = event.detail;
227+
let filteredMetrics = this.allMetrics;
228+
// Apply after filter
229+
if (afterTime) {
230+
filteredMetrics = filteredMetrics.filter(
231+
(e) => e.timestamp && e.timestamp >= afterTime
232+
);
233+
}
234+
// Apply before filter
235+
if (beforeTime) {
236+
filteredMetrics = filteredMetrics.filter(
237+
(e) => e.timestamp && e.timestamp <= beforeTime
238+
);
239+
}
240+
// Apply event type filters
241+
const hiddenTypes = [];
242+
eventTypes.forEach((isChecked, index) => {
243+
if (!isChecked) {
244+
hiddenTypes.push(index);
245+
}
246+
});
247+
if (hiddenTypes.length > 0) {
248+
filteredMetrics = filteredMetrics.filter(
249+
(e) => !hiddenTypes.includes(e.type)
250+
);
251+
}
252+
// Update view
253+
this.filteredMetrics = filteredMetrics;
254+
this.drawTimeline();
255+
}
256+
257+
get timelineClasses() {
258+
return `timeline ${
259+
this.filteredMetrics?.length > 0 ? '' : 'slds-hide'
260+
}`;
261+
}
262+
263+
get eventCountLabel() {
264+
if (this.allMetrics?.length === 0) {
265+
return 'No data to display';
266+
}
267+
if (this.allMetrics?.length !== this.filteredMetrics?.length) {
268+
return `Showing ${this.filteredMetrics?.length} of ${this.allMetrics?.length} items`;
269+
}
270+
return `Showing ${this.allMetrics?.length} items`;
271+
}
272+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0" encoding="UTF-8" ?>
2+
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
3+
<apiVersion>56.0</apiVersion>
4+
<isExposed>false</isExposed>
5+
</LightningComponentBundle>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
.event-types {
2+
display: flex;
3+
flex-wrap: wrap;
4+
}
5+
6+
.event-type {
7+
display: flex;
8+
}
9+
10+
.dot {
11+
height: 25px;
12+
width: 25px;
13+
background-color: #bbb;
14+
border-radius: 50%;
15+
display: inline-block;
16+
}

0 commit comments

Comments
 (0)