Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementing metrics into the status page #58

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
62 changes: 61 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
"@alpinejs/collapse": "^3.13.3",
"@alpinejs/focus": "^3.13.3",
"@alpinejs/ui": "^3.13.3-beta.4",
"alpinejs": "^3.13.3"
"alpinejs": "^3.13.3",
"chart.js": "^4.4.2",
"chartjs-adapter-moment": "^1.0.1",
"moment": "^2.30.1"
}
}
10 changes: 9 additions & 1 deletion resources/js/cachet.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import 'moment';
import Chart from 'chart.js/auto'
import 'chartjs-adapter-moment';

import Alpine from 'alpinejs'

import Anchor from '@alpinejs/anchor'
import Collapse from '@alpinejs/collapse'
import Focus from '@alpinejs/focus'
import Ui from '@alpinejs/ui'

Chart.defaults.color = '#fff';
window.Chart = Chart

Alpine.plugin(Anchor)
Alpine.plugin(Collapse)
Alpine.plugin(Focus)
Alpine.plugin(Ui)

Alpine.start()
window.Alpine = Alpine
Alpine.start()
48 changes: 48 additions & 0 deletions resources/views/components/metric.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
@props([
'metric',
])

@use('\Cachet\Enums\MetricViewEnum')

<div x-data="chart">
<div class="flex flex-col gap-2">
<div class="flex items-center gap-1.5">
<div class="font-semibold leading-6">{{ $metric->name }}</div>

<div x-data x-popover class="flex items-center">
<button x-ref="anchor" x-popover:button>
<x-heroicon-o-question-mark-circle class="size-4 text-zinc-500 dark:text-zinc-300" />
</button>
<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">
<span class="pointer-events-none absolute -left-1 top-1.5 size-4 rotate-45 bg-white"></span>
<p class="relative">{{ $metric->description }}</p>
</div>
</div>

<!-- Period Selector -->
<select x-model="period" class="ml-auto rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-zinc-800 text-gray-900 dark:text-gray-100 text-sm font-medium">
@foreach([MetricViewEnum::last_hour, MetricViewEnum::today, MetricViewEnum::week, MetricViewEnum::month] as $value)
<option value="{{ $value }}">{{ $value->getLabel() }}</option>
@endforeach
</select>
</div>
<canvas x-ref="canvas" height="300" class="ring-1 ring-gray-900/5 dark:ring-gray-100/10 bg-white dark:bg-zinc-800 rounded-md shadow-sm text-white"></canvas>
</div>
</div>

<script>
document.addEventListener('alpine:init', () => {
Alpine.data('chart', () => ({
metric: {{ Js::from($metric) }},
period: {{ Js::from($metric->default_view) }},
points: [
[],
[],
[],
[]
],
chart: null,
init,
}))
})
</script>
56 changes: 56 additions & 0 deletions resources/views/components/metrics.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<script>
const now = new Date();
const previousHour = new Date(now - 60 * 60 * 1000);
const previous24Hours = new Date(now - 24 * 60 * 60 * 1000);
const previous7Days = new Date(now - 7 * 24 * 60 * 60 * 1000);
const previous30Days = new Date(now - 30 * 24 * 60 * 60 * 1000);

function init() {
// Parse metric points
const metricPoints = this.metric.metric_points.map((point) => {
return {
x: new Date(point.x),
y: point.y
}
});

// Filter points based on the selected period
this.points[0] = metricPoints.filter((point) => point.x >= previousHour);
this.points[1] = metricPoints.filter((point) => point.x >= previous24Hours);
this.points[2] = metricPoints.filter((point) => point.x >= previous7Days);
this.points[3] = metricPoints.filter((point) => point.x >= previous30Days);

// Initialize chart
const chart = new Chart(this.$refs.canvas, {
type: 'line',
data: {
datasets: [{
label: this.metric.suffix,
data: this.points[this.period],
fill: false,
borderColor: 'rgb(75, 192, 192)',
tension: 0.1
}],
},
options: {
scales: {
x: {
type: 'timeseries',
}
},
}
});

this.$watch('period', () => {
chart.data.datasets[0].data = this.points[this.period];
chart.update();
});
}
</script>

<div class="flex flex-col gap-8">
@foreach($metrics as $metric)
<x-cachet::metric :metric="$metric" />
<x-cachet::metric :metric="$metric" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this duplication can be removed?

Suggested change
<x-cachet::metric :metric="$metric" />

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the moment, I keep it for debugging purposes (by default, we only have one metric. It allows testing the individual component with two different instances.)

@endforeach
</div>
2 changes: 2 additions & 0 deletions resources/views/status-page/index.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
<x-cachet::component-ungrouped :component="$component" />
@endforeach

<x-cachet::metrics />

@if($schedules->isNotEmpty())
<x-cachet::schedules :schedules="$schedules" />
@endif
Expand Down
53 changes: 53 additions & 0 deletions src/View/Components/Metrics.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace Cachet\View\Components;

use Cachet\Models\Metric;
use Cachet\Settings\AppSettings;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\View\Component;

class Metrics extends Component
{
public function __construct(private AppSettings $appSettings)
{
//
}

public function render(): View
{
$startDate = Carbon::now()->subDays(30);

$metrics = $this->metrics($startDate);

// Convert each metric point to Chart.js format (x, y)
$metrics->each(function ($metric) {
$metric->metricPoints->transform(fn ($point) => [
'x' => $point->created_at->toIso8601String(),
'y' => $point->value,
]);
});

return view('cachet::components.metrics', [
'metrics' => $metrics
]);
}

/**
* Fetch the available metrics and their points.
*/
private function metrics(Carbon $startDate): Collection
{
return Metric::query()
->with([
'metricPoints' => fn ($query) => $query->orderBy('created_at'),
])
->where('visible', '>=', !auth()->check())
->whereHas('metricPoints', fn (Builder $query) => $query->where('created_at', '>=', $startDate))
->orderBy('places', 'asc')
->get();
}
}