Skip to content

Commit

Permalink
feat: add metrics components to frontend
Browse files Browse the repository at this point in the history
using chartjs to render the chart
  • Loading branch information
Sean Connole committed Feb 19, 2025
1 parent 023c332 commit 5ca40be
Show file tree
Hide file tree
Showing 8 changed files with 231 additions and 21 deletions.
44 changes: 43 additions & 1 deletion package-lock.json

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

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,9 @@
"prettier-plugin-tailwindcss": "^0.5.11",
"tailwindcss": "^3.4.13",
"vite": "^5.4"
},
"dependencies": {
"chart.js": "^4.4.7",
"chartjs-adapter-moment": "^1.0.1"
}
}
7 changes: 7 additions & 0 deletions resources/js/cachet.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
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)

window.Alpine = Alpine
Alpine.start()
27 changes: 15 additions & 12 deletions resources/views/components/component-group.blade.php
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
@props(['componentGroup' => null])

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

@if(($incidentCount = $componentGroup->components->sum('incidents_count')) > 0)
<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">
{{ trans_choice('cachet::component_group.incident_count', $incidentCount) }}
</span>
@if (($incidentCount = $componentGroup->components->sum('incidents_count')) > 0)
<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">
{{ trans_choice('cachet::component_group.incident_count', $incidentCount) }}
</span>
@endif
</div>

<div x-disclosure:panel x-collapse class="flex flex-col divide-y bg-white dark:bg-white/5">
<ul class="divide-y dark:divide-zinc-700">
@foreach($componentGroup->components as $component)
<x-cachet::component :component="$component" />
@foreach ($componentGroup->components as $component)
<x-cachet::component :component="$component" />
@endforeach
</ul>
</div>
Expand Down
43 changes: 43 additions & 0 deletions resources/views/components/metric.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
@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 bg-white text-sm font-medium text-gray-900 dark:border-gray-700 dark:bg-zinc-800 dark:text-gray-100">
@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="rounded-md bg-white text-white shadow-sm ring-1 ring-gray-900/5 dark:bg-zinc-800 dark:ring-gray-100/10"></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>
57 changes: 57 additions & 0 deletions resources/views/components/metrics.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<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" />
@endforeach
</div>
17 changes: 9 additions & 8 deletions resources/views/status-page/index.blade.php
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
<x-cachet::cachet>
<x-cachet::header />

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

<x-cachet::about />

@foreach($componentGroups as $componentGroup)
<x-cachet::component-group :component-group="$componentGroup"/>
@foreach ($componentGroups as $componentGroup)
<x-cachet::component-group :component-group="$componentGroup" />
@endforeach

@foreach($ungroupedComponents as $component)
<x-cachet::component-ungrouped :component="$component" />
@foreach ($ungroupedComponents as $component)
<x-cachet::component-ungrouped :component="$component" />
@endforeach

@if($schedules->isNotEmpty())
<x-cachet::schedules :schedules="$schedules" />
<x-cachet::metrics />

@if ($schedules->isNotEmpty())
<x-cachet::schedules :schedules="$schedules" />
@endif

<x-cachet::incident-timeline />
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)

Check failure on line 15 in src/View/Components/Metrics.php

View workflow job for this annotation

GitHub Actions / Static Analysis - P8.4 - L11.x - prefer-lowest

Property Cachet\View\Components\Metrics::$appSettings is never read, only written.

Check failure on line 15 in src/View/Components/Metrics.php

View workflow job for this annotation

GitHub Actions / Static Analysis - P8.4 - L11.x - prefer-stable

Property Cachet\View\Components\Metrics::$appSettings is never read, only written.

Check failure on line 15 in src/View/Components/Metrics.php

View workflow job for this annotation

GitHub Actions / Static Analysis - P8.3 - L11.x - prefer-lowest

Property Cachet\View\Components\Metrics::$appSettings is never read, only written.

Check failure on line 15 in src/View/Components/Metrics.php

View workflow job for this annotation

GitHub Actions / Static Analysis - P8.3 - L11.x - prefer-stable

Property Cachet\View\Components\Metrics::$appSettings is never read, only written.

Check failure on line 15 in src/View/Components/Metrics.php

View workflow job for this annotation

GitHub Actions / Static Analysis - P8.2 - L11.x - prefer-lowest

Property Cachet\View\Components\Metrics::$appSettings is never read, only written.

Check failure on line 15 in src/View/Components/Metrics.php

View workflow job for this annotation

GitHub Actions / Static Analysis - P8.2 - L11.x - prefer-stable

Property Cachet\View\Components\Metrics::$appSettings is never read, only written.
{
//
}

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();
}
}

0 comments on commit 5ca40be

Please sign in to comment.