Skip to content

Commit 5ca40be

Browse files
author
Sean Connole
committed
feat: add metrics components to frontend
using chartjs to render the chart
1 parent 023c332 commit 5ca40be

File tree

8 files changed

+231
-21
lines changed

8 files changed

+231
-21
lines changed

package-lock.json

Lines changed: 43 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 0 deletions
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

Lines changed: 7 additions & 0 deletions
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

Lines changed: 15 additions & 12 deletions
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>
Lines changed: 43 additions & 0 deletions
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="300" class="rounded-md bg-white text-white shadow-sm ring-1 ring-gray-900/5 dark:bg-zinc-800 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>
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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+
function init() {
9+
// Parse metric points
10+
const metricPoints = this.metric.metric_points.map((point) => {
11+
return {
12+
x: new Date(point.x),
13+
y: point.y,
14+
}
15+
})
16+
17+
// Filter points based on the selected period
18+
this.points[0] = metricPoints.filter((point) => point.x >= previousHour)
19+
this.points[1] = metricPoints.filter((point) => point.x >= previous24Hours)
20+
this.points[2] = metricPoints.filter((point) => point.x >= previous7Days)
21+
this.points[3] = metricPoints.filter((point) => point.x >= previous30Days)
22+
23+
// Initialize chart
24+
const chart = new Chart(this.$refs.canvas, {
25+
type: 'line',
26+
data: {
27+
datasets: [
28+
{
29+
label: this.metric.suffix,
30+
data: this.points[this.period],
31+
fill: false,
32+
borderColor: 'rgb(75, 192, 192)',
33+
tension: 0.1,
34+
},
35+
],
36+
},
37+
options: {
38+
scales: {
39+
x: {
40+
type: 'timeseries',
41+
},
42+
},
43+
},
44+
})
45+
46+
this.$watch('period', () => {
47+
chart.data.datasets[0].data = this.points[this.period]
48+
chart.update()
49+
})
50+
}
51+
</script>
52+
53+
<div class="flex flex-col gap-8">
54+
@foreach ($metrics as $metric)
55+
<x-cachet::metric :metric="$metric" />
56+
@endforeach
57+
</div>

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

Lines changed: 9 additions & 8 deletions
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 />

src/View/Components/Metrics.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
namespace Cachet\View\Components;
4+
5+
use Cachet\Models\Metric;
6+
use Cachet\Settings\AppSettings;
7+
use Illuminate\Contracts\View\View;
8+
use Illuminate\Database\Eloquent\Builder;
9+
use Illuminate\Support\Carbon;
10+
use Illuminate\Support\Collection;
11+
use Illuminate\View\Component;
12+
13+
class Metrics extends Component
14+
{
15+
public function __construct(private AppSettings $appSettings)
16+
{
17+
//
18+
}
19+
20+
public function render(): View
21+
{
22+
$startDate = Carbon::now()->subDays(30);
23+
24+
$metrics = $this->metrics($startDate);
25+
26+
// Convert each metric point to Chart.js format (x, y)
27+
$metrics->each(function ($metric) {
28+
$metric->metricPoints->transform(fn ($point) => [
29+
'x' => $point->created_at->toIso8601String(),
30+
'y' => $point->value,
31+
]);
32+
});
33+
34+
return view('cachet::components.metrics', [
35+
'metrics' => $metrics
36+
]);
37+
}
38+
39+
/**
40+
* Fetch the available metrics and their points.
41+
*/
42+
private function metrics(Carbon $startDate): Collection
43+
{
44+
return Metric::query()
45+
->with([
46+
'metricPoints' => fn ($query) => $query->orderBy('created_at'),
47+
])
48+
->where('visible', '>=', !auth()->check())
49+
->whereHas('metricPoints', fn (Builder $query) => $query->where('created_at', '>=', $startDate))
50+
->orderBy('places', 'asc')
51+
->get();
52+
}
53+
}

0 commit comments

Comments
 (0)