From 5ca40be516c4c86bb90453e0f49ea59a4e48511c Mon Sep 17 00:00:00 2001 From: Sean Connole Date: Tue, 18 Feb 2025 22:01:21 -0700 Subject: [PATCH 1/5] feat: add metrics components to frontend using chartjs to render the chart --- package-lock.json | 44 +++++++++++++- package.json | 4 ++ resources/js/cachet.js | 7 +++ .../components/component-group.blade.php | 27 +++++---- resources/views/components/metric.blade.php | 43 ++++++++++++++ resources/views/components/metrics.blade.php | 57 +++++++++++++++++++ resources/views/status-page/index.blade.php | 17 +++--- src/View/Components/Metrics.php | 53 +++++++++++++++++ 8 files changed, 231 insertions(+), 21 deletions(-) create mode 100644 resources/views/components/metric.blade.php create mode 100644 resources/views/components/metrics.blade.php create mode 100644 src/View/Components/Metrics.php diff --git a/package-lock.json b/package-lock.json index a1afcae1..eaba4ec9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,9 +1,13 @@ { - "name": "cachet-core", + "name": "cachethq-core", "lockfileVersion": 3, "requires": true, "packages": { "": { + "dependencies": { + "chart.js": "^4.4.7", + "chartjs-adapter-moment": "^1.0.1" + }, "devDependencies": { "@alpinejs/anchor": "^3.13.3", "@alpinejs/collapse": "^3.13.3", @@ -530,6 +534,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1065,6 +1075,28 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chart.js": { + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.7.tgz", + "integrity": "sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chartjs-adapter-moment": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/chartjs-adapter-moment/-/chartjs-adapter-moment-1.0.1.tgz", + "integrity": "sha512-Uz+nTX/GxocuqXpGylxK19YG4R3OSVf8326D+HwSTsNw1LgzyIGRo+Qujwro1wy6X+soNSnfj5t2vZ+r6EaDmA==", + "license": "MIT", + "peerDependencies": { + "chart.js": ">=3.0.0", + "moment": "^2.10.2" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -1893,6 +1925,16 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "peer": true, + "engines": { + "node": "*" + } + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", diff --git a/package.json b/package.json index b046fb37..58670969 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/resources/js/cachet.js b/resources/js/cachet.js index 50005015..d286c497 100644 --- a/resources/js/cachet.js +++ b/resources/js/cachet.js @@ -1,3 +1,6 @@ +import Chart from 'chart.js/auto' +import 'chartjs-adapter-moment' + import Alpine from 'alpinejs' import Anchor from '@alpinejs/anchor' @@ -5,9 +8,13 @@ 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() diff --git a/resources/views/components/component-group.blade.php b/resources/views/components/component-group.blade.php index b714c40f..b20f93db 100644 --- a/resources/views/components/component-group.blade.php +++ b/resources/views/components/component-group.blade.php @@ -1,12 +1,15 @@ @props(['componentGroup' => null]) {{ \Cachet\Facades\CachetView::renderHook(\Cachet\View\RenderHook::STATUS_PAGE_COMPONENT_GROUPS_BEFORE) }} -
merge(array_filter([ - 'default-open' => $componentGroup->isExpanded(), - ])) - ->class(['overflow-hidden rounded-lg border dark:border-zinc-700']) - }}> +
merge( + array_filter([ + 'default-open' => $componentGroup->isExpanded(), + ]), + ) + ->class(['overflow-hidden rounded-lg border dark:border-zinc-700']) +}}>
- @if(($incidentCount = $componentGroup->components->sum('incidents_count')) > 0) - - {{ trans_choice('cachet::component_group.incident_count', $incidentCount) }} - + @if (($incidentCount = $componentGroup->components->sum('incidents_count')) > 0) + + {{ trans_choice('cachet::component_group.incident_count', $incidentCount) }} + @endif
    - @foreach($componentGroup->components as $component) - + @foreach ($componentGroup->components as $component) + @endforeach
diff --git a/resources/views/components/metric.blade.php b/resources/views/components/metric.blade.php new file mode 100644 index 00000000..9b6f3627 --- /dev/null +++ b/resources/views/components/metric.blade.php @@ -0,0 +1,43 @@ +@props([ + 'metric', +]) + +@use('\Cachet\Enums\MetricViewEnum') + +
+
+
+
{{ $metric->name }}
+ +
+ +
+ +

{{ $metric->description }}

+
+
+ + + +
+ +
+
+ + diff --git a/resources/views/components/metrics.blade.php b/resources/views/components/metrics.blade.php new file mode 100644 index 00000000..16a7e0d4 --- /dev/null +++ b/resources/views/components/metrics.blade.php @@ -0,0 +1,57 @@ + + +
+ @foreach ($metrics as $metric) + + @endforeach +
diff --git a/resources/views/status-page/index.blade.php b/resources/views/status-page/index.blade.php index 5fef4e98..bb9092e9 100644 --- a/resources/views/status-page/index.blade.php +++ b/resources/views/status-page/index.blade.php @@ -1,21 +1,22 @@ -
+
- - @foreach($componentGroups as $componentGroup) - + @foreach ($componentGroups as $componentGroup) + @endforeach - @foreach($ungroupedComponents as $component) - + @foreach ($ungroupedComponents as $component) + @endforeach - @if($schedules->isNotEmpty()) - + + + @if ($schedules->isNotEmpty()) + @endif diff --git a/src/View/Components/Metrics.php b/src/View/Components/Metrics.php new file mode 100644 index 00000000..d12b52fc --- /dev/null +++ b/src/View/Components/Metrics.php @@ -0,0 +1,53 @@ +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(); + } +} From 11b880b282f44b14b140b10cf95e8c9219a2e871 Mon Sep 17 00:00:00 2001 From: Sean Connole Date: Fri, 21 Feb 2025 10:50:41 -0700 Subject: [PATCH 2/5] feat: add metrics component to frontend --- resources/views/components/metrics.blade.php | 19 +++++++++++++++++++ src/View/Components/Metrics.php | 4 +++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/resources/views/components/metrics.blade.php b/resources/views/components/metrics.blade.php index 16a7e0d4..786a7518 100644 --- a/resources/views/components/metrics.blade.php +++ b/resources/views/components/metrics.blade.php @@ -5,6 +5,15 @@ const previous7Days = new Date(now - 7 * 24 * 60 * 60 * 1000) const previous30Days = new Date(now - 30 * 24 * 60 * 60 * 1000) + const MetricView = {{ + Js::from([ + 'last_hour' => \Cachet\Enums\MetricViewEnum::last_hour->value, + 'today' => \Cachet\Enums\MetricViewEnum::today->value, + 'week' => \Cachet\Enums\MetricViewEnum::week->value, + 'month' => \Cachet\Enums\MetricViewEnum::month->value, + ]) + }} + function init() { // Parse metric points const metricPoints = this.metric.metric_points.map((point) => { @@ -45,8 +54,18 @@ function init() { this.$watch('period', () => { chart.data.datasets[0].data = this.points[this.period] + chart.options.scales.x.time.unit = getTimeUnit(this.period) + chart.update() }) + + function getTimeUnit(period) { + if (period == MetricView.last_hour) return 'minute' + if (period == MetricView.today) return 'hour' + if (period == MetricView.week) return 'week' + if (period == MetricView.month) return 'month' + return 'day' + } } diff --git a/src/View/Components/Metrics.php b/src/View/Components/Metrics.php index d12b52fc..a500e050 100644 --- a/src/View/Components/Metrics.php +++ b/src/View/Components/Metrics.php @@ -26,7 +26,7 @@ public function render(): View // Convert each metric point to Chart.js format (x, y) $metrics->each(function ($metric) { $metric->metricPoints->transform(fn ($point) => [ - 'x' => $point->created_at->toIso8601String(), + 'x' => $point->created_at->utc(), 'y' => $point->value, ]); }); @@ -45,6 +45,8 @@ private function metrics(Carbon $startDate): Collection ->with([ 'metricPoints' => fn ($query) => $query->orderBy('created_at'), ]) + ->where('display_chart', '=', 1) + ->where('visible', '=', 1) ->where('visible', '>=', !auth()->check()) ->whereHas('metricPoints', fn (Builder $query) => $query->where('created_at', '>=', $startDate)) ->orderBy('places', 'asc') From e2c8da4d012c8c024b31d2c1d53f575589cb08d8 Mon Sep 17 00:00:00 2001 From: Sean Connole Date: Fri, 21 Feb 2025 11:59:08 -0700 Subject: [PATCH 3/5] chore: update color scheme to match tailwind colors --- resources/views/components/metric.blade.php | 2 +- resources/views/components/metrics.blade.php | 43 +++++++++++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/resources/views/components/metric.blade.php b/resources/views/components/metric.blade.php index 9b6f3627..6bc8b6c5 100644 --- a/resources/views/components/metric.blade.php +++ b/resources/views/components/metric.blade.php @@ -26,7 +26,7 @@ @endforeach
- +
diff --git a/resources/views/components/metrics.blade.php b/resources/views/components/metrics.blade.php index 786a7518..43bdb34e 100644 --- a/resources/views/components/metrics.blade.php +++ b/resources/views/components/metrics.blade.php @@ -14,6 +14,24 @@ ]) }} + function getCssVar(name) { + return getComputedStyle(document.documentElement).getPropertyValue(name).trim() + } + + function getThemeColors() { + const accent = `rgba(${getCssVar('--accent')}, 1)` // Full opacity + const accentContent = `rgba(${getCssVar('--accent-content')}, 1)` + const accentBackground = `rgba(${getCssVar('--accent-background')}, 0.2)` + + return { + fontColor: accentContent, + backgroundColors: [accent, accentBackground], // Use accent colors dynamically + borderColor: accent, + } + } + + let themeColors = getThemeColors() + function init() { // Parse metric points const metricPoints = this.metric.metric_points.map((point) => { @@ -38,7 +56,8 @@ function init() { label: this.metric.suffix, data: this.points[this.period], fill: false, - borderColor: 'rgb(75, 192, 192)', + backgroundColor: themeColors.backgroundColors, + borderColor: themeColors.borderColor, tension: 0.1, }, ], @@ -46,8 +65,16 @@ function init() { options: { scales: { x: { + ticks: { + color: themeColors.fontColor, + }, type: 'timeseries', }, + y: { + ticks: { + color: themeColors.fontColor, + }, + }, }, }, }) @@ -67,6 +94,20 @@ function getTimeUnit(period) { return 'day' } } + + const observer = new MutationObserver(() => { + themeColors = getThemeColors() + + myChart.data.datasets[0].backgroundColor = themeColors.backgroundColors + myChart.data.datasets[0].borderColor = themeColors.borderColor + myChart.options.plugins.legend.labels.color = themeColors.fontColor + myChart.options.plugins.tooltip.bodyColor = themeColors.fontColor + myChart.options.plugins.tooltip.titleColor = themeColors.fontColor + myChart.options.scales.x.ticks.color = themeColors.fontColor + myChart.options.scales.y.ticks.color = themeColors.fontColor + + myChart.update() + })
From 9eabe6f8ceddb360db458d8a196ba05b9e1f0a62 Mon Sep 17 00:00:00 2001 From: Sean Connole Date: Fri, 21 Feb 2025 12:26:57 -0700 Subject: [PATCH 4/5] fix: theme updating for font color --- resources/views/components/metrics.blade.php | 42 ++++++++++++-------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/resources/views/components/metrics.blade.php b/resources/views/components/metrics.blade.php index 43bdb34e..1e6f6f92 100644 --- a/resources/views/components/metrics.blade.php +++ b/resources/views/components/metrics.blade.php @@ -18,14 +18,22 @@ function getCssVar(name) { return getComputedStyle(document.documentElement).getPropertyValue(name).trim() } + function getFontColor() { + if (window.matchMedia('(prefers-color-scheme: dark)').matches === true) { + return `rgba(${getCssVar('--gray-100')}, 1)` + } + + return `rgba(${getCssVar('--gray-800')}, 1)` + } + function getThemeColors() { - const accent = `rgba(${getCssVar('--accent')}, 1)` // Full opacity - const accentContent = `rgba(${getCssVar('--accent-content')}, 1)` + const fontColor = getFontColor() + const accent = `rgba(${getCssVar('--accent')}, 1)` const accentBackground = `rgba(${getCssVar('--accent-background')}, 0.2)` return { - fontColor: accentContent, - backgroundColors: [accent, accentBackground], // Use accent colors dynamically + fontColor: fontColor, + backgroundColors: [accent, accentBackground], borderColor: accent, } } @@ -93,21 +101,23 @@ function getTimeUnit(period) { if (period == MetricView.month) return 'month' return 'day' } - } - const observer = new MutationObserver(() => { - themeColors = getThemeColors() + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { + themeColors = getThemeColors() - myChart.data.datasets[0].backgroundColor = themeColors.backgroundColors - myChart.data.datasets[0].borderColor = themeColors.borderColor - myChart.options.plugins.legend.labels.color = themeColors.fontColor - myChart.options.plugins.tooltip.bodyColor = themeColors.fontColor - myChart.options.plugins.tooltip.titleColor = themeColors.fontColor - myChart.options.scales.x.ticks.color = themeColors.fontColor - myChart.options.scales.y.ticks.color = themeColors.fontColor + console.log(themeColors) - myChart.update() - }) + chart.data.datasets[0].backgroundColor = themeColors.backgroundColors + chart.data.datasets[0].borderColor = themeColors.borderColor + chart.options.plugins.legend.labels.color = themeColors.fontColor + chart.options.plugins.tooltip.bodyColor = themeColors.fontColor + chart.options.plugins.tooltip.titleColor = themeColors.fontColor + chart.options.scales.x.ticks.color = themeColors.fontColor + chart.options.scales.y.ticks.color = themeColors.fontColor + + chart.update() + }) + }
From f21f265d141df2be291ca8b17f8012ad4e11ee86 Mon Sep 17 00:00:00 2001 From: Sean Connole Date: Fri, 21 Feb 2025 12:34:35 -0700 Subject: [PATCH 5/5] fix: remove console log --- resources/views/components/metrics.blade.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/resources/views/components/metrics.blade.php b/resources/views/components/metrics.blade.php index 1e6f6f92..52c3616f 100644 --- a/resources/views/components/metrics.blade.php +++ b/resources/views/components/metrics.blade.php @@ -105,8 +105,6 @@ function getTimeUnit(period) { window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { themeColors = getThemeColors() - console.log(themeColors) - chart.data.datasets[0].backgroundColor = themeColors.backgroundColors chart.data.datasets[0].borderColor = themeColors.borderColor chart.options.plugins.legend.labels.color = themeColors.fontColor