Skip to content

Commit ca321a1

Browse files
sconnoleSean Connole
and
Sean Connole
authored
feat: add metrics components to frontend (#246)
Co-authored-by: Sean Connole <[email protected]>
1 parent 83fde88 commit ca321a1

File tree

8 files changed

+301
-21
lines changed

8 files changed

+301
-21
lines changed

package-lock.json

+43-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+4
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,9 @@
2323
"prettier-plugin-tailwindcss": "^0.5.11",
2424
"tailwindcss": "^3.4.13",
2525
"vite": "^5.4"
26+
},
27+
"dependencies": {
28+
"chart.js": "^4.4.7",
29+
"chartjs-adapter-moment": "^1.0.1"
2630
}
2731
}

resources/js/cachet.js

+7
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
1+
import Chart from 'chart.js/auto'
2+
import 'chartjs-adapter-moment'
3+
14
import Alpine from 'alpinejs'
25

36
import Anchor from '@alpinejs/anchor'
47
import Collapse from '@alpinejs/collapse'
58
import Focus from '@alpinejs/focus'
69
import Ui from '@alpinejs/ui'
710

11+
Chart.defaults.color = '#fff'
12+
window.Chart = Chart
13+
814
Alpine.plugin(Anchor)
915
Alpine.plugin(Collapse)
1016
Alpine.plugin(Focus)
1117
Alpine.plugin(Ui)
1218

19+
window.Alpine = Alpine
1320
Alpine.start()

resources/views/components/component-group.blade.php

+15-12
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
@props(['componentGroup' => null])
22

33
{{ \Cachet\Facades\CachetView::renderHook(\Cachet\View\RenderHook::STATUS_PAGE_COMPONENT_GROUPS_BEFORE) }}
4-
<div x-data x-disclosure {{ $attributes
5-
->merge(array_filter([
6-
'default-open' => $componentGroup->isExpanded(),
7-
]))
8-
->class(['overflow-hidden rounded-lg border dark:border-zinc-700'])
9-
}}>
4+
<div x-data x-disclosure {{
5+
$attributes
6+
->merge(
7+
array_filter([
8+
'default-open' => $componentGroup->isExpanded(),
9+
]),
10+
)
11+
->class(['overflow-hidden rounded-lg border dark:border-zinc-700'])
12+
}}>
1013
<div class="flex items-center justify-between bg-white p-4 dark:border-zinc-700 dark:bg-white/5">
1114
<button x-disclosure:button class="flex items-center gap-2 text-zinc-500 dark:text-zinc-300">
1215
<h3 class="text-lg font-semibold">
@@ -15,17 +18,17 @@
1518
<x-heroicon-o-chevron-up ::class="!$disclosure.isOpen && 'rotate-180'" class="size-4 transition" />
1619
</button>
1720

18-
@if(($incidentCount = $componentGroup->components->sum('incidents_count')) > 0)
19-
<span class="rounded border border-zinc-800 px-2 py-1 text-xs font-semibold text-zinc-800 dark:border-zinc-600 dark:text-zinc-400">
20-
{{ trans_choice('cachet::component_group.incident_count', $incidentCount) }}
21-
</span>
21+
@if (($incidentCount = $componentGroup->components->sum('incidents_count')) > 0)
22+
<span class="rounded border border-zinc-800 px-2 py-1 text-xs font-semibold text-zinc-800 dark:border-zinc-600 dark:text-zinc-400">
23+
{{ trans_choice('cachet::component_group.incident_count', $incidentCount) }}
24+
</span>
2225
@endif
2326
</div>
2427

2528
<div x-disclosure:panel x-collapse class="flex flex-col divide-y bg-white dark:bg-white/5">
2629
<ul class="divide-y dark:divide-zinc-700">
27-
@foreach($componentGroup->components as $component)
28-
<x-cachet::component :component="$component" />
30+
@foreach ($componentGroup->components as $component)
31+
<x-cachet::component :component="$component" />
2932
@endforeach
3033
</ul>
3134
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
@props([
2+
'metric',
3+
])
4+
5+
@use('\Cachet\Enums\MetricViewEnum')
6+
7+
<div x-data="chart">
8+
<div class="flex flex-col gap-2">
9+
<div class="flex items-center gap-1.5">
10+
<div class="font-semibold leading-6">{{ $metric->name }}</div>
11+
12+
<div x-data x-popover class="flex items-center">
13+
<button x-ref="anchor" x-popover:button>
14+
<x-heroicon-o-question-mark-circle class="size-4 text-zinc-500 dark:text-zinc-300" />
15+
</button>
16+
<div x-popover:panel x-cloak x-transition.opacity x-anchor.right.offset.8="$refs.anchor" class="rounded bg-white px-2 py-1 text-xs font-medium text-zinc-800 drop-shadow dark:text-zinc-800">
17+
<span class="pointer-events-none absolute -left-1 top-1.5 size-4 rotate-45 bg-white"></span>
18+
<p class="relative">{{ $metric->description }}</p>
19+
</div>
20+
</div>
21+
22+
<!-- Period Selector -->
23+
<select x-model="period" class="ml-auto rounded-md border border-gray-300 bg-white text-sm font-medium text-gray-900 dark:border-gray-700 dark:bg-zinc-800 dark:text-gray-100">
24+
@foreach ([MetricViewEnum::last_hour, MetricViewEnum::today, MetricViewEnum::week, MetricViewEnum::month] as $value)
25+
<option value="{{ $value }}">{{ $value->getLabel() }}</option>
26+
@endforeach
27+
</select>
28+
</div>
29+
<canvas x-ref="canvas" height="380" class="text-gray rounded-md bg-white p-3 shadow-sm ring-1 ring-gray-900/5 dark:bg-zinc-800 dark:text-white dark:ring-gray-100/10"></canvas>
30+
</div>
31+
</div>
32+
33+
<script>
34+
document.addEventListener('alpine:init', () => {
35+
Alpine.data('chart', () => ({
36+
metric: {{ Js::from($metric) }},
37+
period: {{ Js::from($metric->default_view) }},
38+
points: [[], [], [], []],
39+
chart: null,
40+
init,
41+
}))
42+
})
43+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<script>
2+
const now = new Date()
3+
const previousHour = new Date(now - 60 * 60 * 1000)
4+
const previous24Hours = new Date(now - 24 * 60 * 60 * 1000)
5+
const previous7Days = new Date(now - 7 * 24 * 60 * 60 * 1000)
6+
const previous30Days = new Date(now - 30 * 24 * 60 * 60 * 1000)
7+
8+
const MetricView = {{
9+
Js::from([
10+
'last_hour' => \Cachet\Enums\MetricViewEnum::last_hour->value,
11+
'today' => \Cachet\Enums\MetricViewEnum::today->value,
12+
'week' => \Cachet\Enums\MetricViewEnum::week->value,
13+
'month' => \Cachet\Enums\MetricViewEnum::month->value,
14+
])
15+
}}
16+
17+
function getCssVar(name) {
18+
return getComputedStyle(document.documentElement).getPropertyValue(name).trim()
19+
}
20+
21+
function getFontColor() {
22+
if (window.matchMedia('(prefers-color-scheme: dark)').matches === true) {
23+
return `rgba(${getCssVar('--gray-100')}, 1)`
24+
}
25+
26+
return `rgba(${getCssVar('--gray-800')}, 1)`
27+
}
28+
29+
function getThemeColors() {
30+
const fontColor = getFontColor()
31+
const accent = `rgba(${getCssVar('--accent')}, 1)`
32+
const accentBackground = `rgba(${getCssVar('--accent-background')}, 0.2)`
33+
34+
return {
35+
fontColor: fontColor,
36+
backgroundColors: [accent, accentBackground],
37+
borderColor: accent,
38+
}
39+
}
40+
41+
let themeColors = getThemeColors()
42+
43+
function init() {
44+
// Parse metric points
45+
const metricPoints = this.metric.metric_points.map((point) => {
46+
return {
47+
x: new Date(point.x),
48+
y: point.y,
49+
}
50+
})
51+
52+
// Filter points based on the selected period
53+
this.points[0] = metricPoints.filter((point) => point.x >= previousHour)
54+
this.points[1] = metricPoints.filter((point) => point.x >= previous24Hours)
55+
this.points[2] = metricPoints.filter((point) => point.x >= previous7Days)
56+
this.points[3] = metricPoints.filter((point) => point.x >= previous30Days)
57+
58+
// Initialize chart
59+
const chart = new Chart(this.$refs.canvas, {
60+
type: 'line',
61+
data: {
62+
datasets: [
63+
{
64+
label: this.metric.suffix,
65+
data: this.points[this.period],
66+
fill: false,
67+
backgroundColor: themeColors.backgroundColors,
68+
borderColor: themeColors.borderColor,
69+
tension: 0.1,
70+
},
71+
],
72+
},
73+
options: {
74+
scales: {
75+
x: {
76+
ticks: {
77+
color: themeColors.fontColor,
78+
},
79+
type: 'timeseries',
80+
},
81+
y: {
82+
ticks: {
83+
color: themeColors.fontColor,
84+
},
85+
},
86+
},
87+
},
88+
})
89+
90+
this.$watch('period', () => {
91+
chart.data.datasets[0].data = this.points[this.period]
92+
chart.options.scales.x.time.unit = getTimeUnit(this.period)
93+
94+
chart.update()
95+
})
96+
97+
function getTimeUnit(period) {
98+
if (period == MetricView.last_hour) return 'minute'
99+
if (period == MetricView.today) return 'hour'
100+
if (period == MetricView.week) return 'week'
101+
if (period == MetricView.month) return 'month'
102+
return 'day'
103+
}
104+
105+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
106+
themeColors = getThemeColors()
107+
108+
chart.data.datasets[0].backgroundColor = themeColors.backgroundColors
109+
chart.data.datasets[0].borderColor = themeColors.borderColor
110+
chart.options.plugins.legend.labels.color = themeColors.fontColor
111+
chart.options.plugins.tooltip.bodyColor = themeColors.fontColor
112+
chart.options.plugins.tooltip.titleColor = themeColors.fontColor
113+
chart.options.scales.x.ticks.color = themeColors.fontColor
114+
chart.options.scales.y.ticks.color = themeColors.fontColor
115+
116+
chart.update()
117+
})
118+
}
119+
</script>
120+
121+
<div class="flex flex-col gap-8">
122+
@foreach ($metrics as $metric)
123+
<x-cachet::metric :metric="$metric" />
124+
@endforeach
125+
</div>

resources/views/status-page/index.blade.php

+9-8
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
<x-cachet::cachet>
22
<x-cachet::header />
33

4-
<div class="container mx-auto max-w-5xl px-4 py-10 sm:px-6 lg:px-8 flex flex-col space-y-6">
4+
<div class="container mx-auto flex max-w-5xl flex-col space-y-6 px-4 py-10 sm:px-6 lg:px-8">
55
<x-cachet::status-bar />
66

77
<x-cachet::about />
8-
9-
@foreach($componentGroups as $componentGroup)
10-
<x-cachet::component-group :component-group="$componentGroup"/>
8+
@foreach ($componentGroups as $componentGroup)
9+
<x-cachet::component-group :component-group="$componentGroup" />
1110
@endforeach
1211

13-
@foreach($ungroupedComponents as $component)
14-
<x-cachet::component-ungrouped :component="$component" />
12+
@foreach ($ungroupedComponents as $component)
13+
<x-cachet::component-ungrouped :component="$component" />
1514
@endforeach
1615

17-
@if($schedules->isNotEmpty())
18-
<x-cachet::schedules :schedules="$schedules" />
16+
<x-cachet::metrics />
17+
18+
@if ($schedules->isNotEmpty())
19+
<x-cachet::schedules :schedules="$schedules" />
1920
@endif
2021

2122
<x-cachet::incident-timeline />

0 commit comments

Comments
 (0)