Skip to content

Commit d214793

Browse files
committed
add more panels
1 parent 4c63109 commit d214793

11 files changed

+494
-67
lines changed

apps/web/src/components/tools/pagerduty/dashboard.tsx

Lines changed: 111 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ import { useState, useEffect, useCallback } from 'react'
55
import { pipe } from '@/lib/tinybird'
66
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
77
import { formatDuration, formatNumber, formatPercentage } from '@/lib/utils'
8-
import Overview from '@/components/overview-chart'
8+
import Overview from '@/components/tools/pagerduty/overview-chart'
99
import BarList from '@/components/bar-list'
1010
import { DateRangePicker, DateRange } from '@/components/ui/date-range-picker'
1111
import { startOfDay, endOfDay, format } from 'date-fns'
1212
import MetricCard from '@/components/metric-card'
13-
import IncidentCategoriesTable from "@/components/incident-categories-table"
13+
import IncidentCategoriesTable from "@/components/tools/pagerduty/incident-categories-table"
1414
import { Filter } from './filters'
15+
import InterruptionsChart from "@/components/tools/pagerduty/interruptions-chart"
16+
import ResponseEffortChart from "@/components/tools/pagerduty/response-effort-chart"
17+
import SleepInterruptions from "@/components/tools/pagerduty/sleep-interruptions"
1518

1619
interface IncidentType {
1720
title: string
@@ -22,6 +25,14 @@ interface IncidentType {
2225
example_title: string
2326
}
2427

28+
interface Responder {
29+
html_url: string;
30+
id: string;
31+
self: string;
32+
summary: string;
33+
type: string;
34+
}
35+
2536
export default function PagerDutyDashboard() {
2637
const [token] = useQueryState('token')
2738
const [dateRange, setDateRange] = useState<DateRange>({
@@ -52,9 +63,10 @@ export default function PagerDutyDashboard() {
5263
high_urgency_rate: number
5364
}>>([])
5465
const [responders, setResponders] = useState<Array<{
55-
responder: string
56-
responses: number
57-
avg_response_time: number
66+
responder: Responder[];
67+
responses: number;
68+
acknowledgements: number;
69+
avg_response_time: number;
5870
}>>([])
5971
const [statusDistribution, setStatusDistribution] = useState<Array<{
6072
event_type: string
@@ -65,6 +77,32 @@ export default function PagerDutyDashboard() {
6577
const pageSize = 10
6678
const [incidentTypes, setIncidentTypes] = useState<IncidentType[]>([])
6779
const [selectedService, setSelectedService] = useState<string>()
80+
const [interruptions, setInterruptions] = useState<Array<{
81+
day: string
82+
interruptions: number
83+
day_type: 'business' | 'off'
84+
}>>([])
85+
const [responseEffort, setResponseEffort] = useState<Array<{
86+
responder: Array<{
87+
html_url: string
88+
id: string
89+
self: string
90+
summary: string
91+
type: string
92+
}>
93+
hours: number
94+
}>>([])
95+
const [sleepInterruptions, setSleepInterruptions] = useState<Array<{
96+
responder: Array<{
97+
html_url: string
98+
id: string
99+
self: string
100+
summary: string
101+
type: string
102+
}>
103+
interruptions: number
104+
avg_response_time: number
105+
}>>([])
68106

69107
const fetchIncidentTypes = useCallback(async (newPage: number) => {
70108
if (!token) return
@@ -116,15 +154,21 @@ export default function PagerDutyDashboard() {
116154
statusDistributionData,
117155
resolutionTimesData,
118156
serviceDistributionData,
119-
incidentTypesData
157+
incidentTypesData,
158+
interruptionsData,
159+
responseEffortData,
160+
sleepInterruptionsData
120161
] = await Promise.all([
121162
pipe(token, 'pagerduty_incident_metrics', baseParams),
122163
pipe(token, 'pagerduty_incidents_over_time', baseParams),
123164
pipe(token, 'pagerduty_responders', baseParams),
124165
pipe(token, 'pagerduty_status_distribution', baseParams),
125166
pipe(token, 'pagerduty_resolution_times', baseParams),
126167
pipe(token, 'pagerduty_service_distribution', baseParams),
127-
pipe(token, 'pagerduty_incident_types', baseParams)
168+
pipe(token, 'pagerduty_incident_types', baseParams),
169+
pipe(token, 'pagerduty_interruptions', baseParams),
170+
pipe(token, 'pagerduty_response_effort', baseParams),
171+
pipe(token, 'pagerduty_sleep_interruptions', baseParams)
128172
])
129173

130174
setMetrics((incidentMetrics?.data?.[0] || {
@@ -143,9 +187,10 @@ export default function PagerDutyDashboard() {
143187
escalated: number
144188
}>)
145189
setResponders(respondersData.data as Array<{
146-
responder: string
147-
responses: number
148-
avg_response_time: number
190+
responder: Responder[];
191+
responses: number;
192+
acknowledgements: number;
193+
avg_response_time: number;
149194
}>)
150195
setStatusDistribution(statusDistributionData.data as Array<{
151196
event_type: string
@@ -166,6 +211,9 @@ export default function PagerDutyDashboard() {
166211
high_urgency_rate: number
167212
}>)
168213
setIncidentTypes(incidentTypesData.data as IncidentType[])
214+
setInterruptions(interruptionsData.data)
215+
setResponseEffort(responseEffortData.data)
216+
setSleepInterruptions(sleepInterruptionsData.data)
169217

170218
} catch (error) {
171219
console.error('Failed to fetch metrics:', error)
@@ -270,9 +318,9 @@ export default function PagerDutyDashboard() {
270318
data={incidentsOverTime}
271319
categories={['triggered', 'resolved', 'escalated']}
272320
colors={{
273-
'triggered': '#dc2626', // Red-600
274-
'resolved': '#16a34a', // Green-600
275-
'escalated': '#d97706' // Amber-600
321+
'triggered': 'hsl(var(--primary))', // Red-600
322+
'resolved': '#93c5fd', // Green-600
323+
'escalated': '#6b7280' // Amber-600
276324
}}
277325
index="hour"
278326
valueKey="count"
@@ -282,31 +330,65 @@ export default function PagerDutyDashboard() {
282330
</Card>
283331

284332
{/* Response Analysis */}
285-
<div className="grid gap-4 grid-cols-3">
333+
<div className="grid gap-4 grid-cols-2">
286334
<Card>
287335
<CardHeader>
288-
<CardTitle>Response Team Activity</CardTitle>
336+
<CardTitle>Oncall Team Activity</CardTitle>
289337
</CardHeader>
290338
<CardContent className="max-h-[400px] overflow-auto">
291339
<BarList
292-
data={(responders || []).map((item) => ({
293-
name: item.responder,
294-
value: item.responses,
295-
extra: `${formatDuration(item.avg_response_time)} avg response time`,
296-
}))}
340+
data={responders
341+
.reduce((acc, item) => {
342+
if (item.responder.length > 1) return acc;
343+
344+
const name = item.responder[0]?.summary || 'Unknown'
345+
const existing = acc.find(x => x.name === name)
346+
if (existing) {
347+
existing.value += item.responses
348+
existing.acknowledgements += item.acknowledgements
349+
existing.avgTime = (existing.avgTime * existing.count + item.avg_response_time) / (existing.count + 1)
350+
existing.count++
351+
} else {
352+
acc.push({
353+
name,
354+
value: item.responses,
355+
acknowledgements: item.acknowledgements,
356+
avgTime: item.avg_response_time,
357+
count: 1
358+
})
359+
}
360+
return acc
361+
}, [] as Array<{
362+
name: string
363+
value: number
364+
acknowledgements: number
365+
avgTime: number
366+
count: number
367+
}>)
368+
.map(item => ({
369+
name: item.name,
370+
value: item.value,
371+
extra: `${item.acknowledgements} acks • ${formatDuration(item.avgTime)} avg response time`
372+
}))
373+
}
297374
/>
298375
</CardContent>
299376
</Card>
300-
<div className="col-span-2">
301-
<IncidentCategoriesTable
302-
data={incidentTypes || []}
303-
page={page}
304-
onPageChange={handlePageChange}
305-
pageSize={pageSize}
306-
isLoading={isTableLoading}
307-
/>
308-
</div>
377+
<ResponseEffortChart data={responseEffort} />
309378
</div>
379+
380+
<div className="grid gap-4 grid-cols-2">
381+
<InterruptionsChart data={interruptions} />
382+
<SleepInterruptions data={sleepInterruptions} />
383+
</div>
384+
385+
<IncidentCategoriesTable
386+
data={incidentTypes || []}
387+
page={page}
388+
onPageChange={handlePageChange}
389+
pageSize={pageSize}
390+
isLoading={isTableLoading}
391+
/>
310392
</div>
311393
)
312394
}

apps/web/src/components/incident-categories-table.tsx renamed to apps/web/src/components/tools/pagerduty/incident-categories-table.tsx

File renamed without changes.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
2+
import { Bar, BarChart, CartesianGrid, ResponsiveContainer, XAxis, YAxis, Tooltip, Legend } from "recharts"
3+
import { format } from "date-fns"
4+
5+
interface InterruptionsChartProps {
6+
data: Array<{
7+
day: string
8+
interruptions: number
9+
day_type: 'business' | 'off'
10+
}>
11+
}
12+
13+
export default function InterruptionsChart({ data }: InterruptionsChartProps) {
14+
return (
15+
<Card>
16+
<CardHeader>
17+
<CardTitle className="flex items-center gap-2">
18+
Interruptions
19+
</CardTitle>
20+
</CardHeader>
21+
<CardContent className="h-[300px]">
22+
<ResponsiveContainer width="100%" height="100%">
23+
<BarChart data={data}>
24+
<CartesianGrid
25+
horizontal={true}
26+
vertical={false}
27+
className="stroke-muted"
28+
/>
29+
<XAxis
30+
dataKey="day"
31+
tickFormatter={(time) => format(new Date(time), 'MMM d')}
32+
tickLine={false}
33+
axisLine={false}
34+
tickMargin={8}
35+
/>
36+
<YAxis
37+
tickLine={false}
38+
axisLine={false}
39+
tickMargin={8}
40+
width={70}
41+
/>
42+
<Tooltip
43+
labelFormatter={(value) => new Date(value).toLocaleString()}
44+
formatter={(value, name) => [value, name.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ')]}
45+
/>
46+
<Legend />
47+
<Bar dataKey="business_hours" stackId="a" fill="hsl(var(--primary))" name="Business Hours" />
48+
<Bar dataKey="sleep_hours" stackId="a" fill="#93c5fd" name="Sleep Hours" />
49+
<Bar dataKey="off_hours" stackId="a" fill="#6b7280" name="Off Hours" />
50+
</BarChart>
51+
</ResponsiveContainer>
52+
</CardContent>
53+
</Card>
54+
)
55+
}

apps/web/src/components/overview-chart.tsx renamed to apps/web/src/components/tools/pagerduty/overview-chart.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"use client"
22

3-
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend } from "recharts"
3+
import { BarChart, Bar, CartesianGrid, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend } from "recharts"
4+
import { format } from "date-fns"
5+
46

57
interface OverviewProps {
68
data: Array<{
@@ -20,12 +22,19 @@ export default function Overview({ data, categories, colors }: OverviewProps) {
2022
return (
2123
<ResponsiveContainer width="100%" height={350}>
2224
<BarChart data={data}>
25+
<CartesianGrid
26+
horizontal={true}
27+
vertical={false}
28+
className="stroke-muted"
29+
/>
2330
<XAxis
2431
dataKey="hour"
2532
stroke="var(--muted-foreground)"
2633
fontSize={12}
2734
tickLine={false}
2835
axisLine={false}
36+
tickFormatter={(time) => format(new Date(time), 'MMM d, HH:00')}
37+
2938
/>
3039
<YAxis
3140
stroke="var(--muted-foreground)"
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
2+
import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis, Tooltip, CartesianGrid } from "recharts"
3+
4+
interface Responder {
5+
html_url: string
6+
id: string
7+
self: string
8+
summary: string
9+
type: string
10+
}
11+
12+
interface ResponseEffortChartProps {
13+
data: Array<{
14+
responder: Responder[]
15+
hours: number
16+
}>
17+
}
18+
19+
export default function ResponseEffortChart({ data }: ResponseEffortChartProps) {
20+
const chartData = data.map(item => ({
21+
responder: item.responder[0].summary,
22+
hours: item.hours
23+
}))
24+
25+
return (
26+
<Card>
27+
<CardHeader>
28+
<CardTitle>Response Effort by Responder</CardTitle>
29+
</CardHeader>
30+
<CardContent className="h-[300px]">
31+
<ResponsiveContainer width="100%" height="100%">
32+
<BarChart
33+
data={chartData}
34+
layout="vertical"
35+
margin={{ top: 5, right: 30, left: 100, bottom: 5 }}
36+
>
37+
<CartesianGrid
38+
horizontal={false}
39+
vertical={true}
40+
className="stroke-muted"
41+
/>
42+
<XAxis type="number" unit=" hours"
43+
tickLine={false}
44+
axisLine={false}
45+
tickMargin={8}
46+
/>
47+
<YAxis
48+
type="category"
49+
dataKey="responder"
50+
width={100}
51+
tickLine={false}
52+
axisLine={false}
53+
tickMargin={8}
54+
/>
55+
<Tooltip
56+
formatter={(value: number) => [`${value} hours`, 'Time spent']}
57+
/>
58+
<Bar
59+
dataKey="hours"
60+
fill="hsl(var(--primary))"
61+
radius={[0, 4, 4, 0]}
62+
/>
63+
</BarChart>
64+
</ResponsiveContainer>
65+
</CardContent>
66+
</Card>
67+
)
68+
}

0 commit comments

Comments
 (0)