Skip to content

Commit cf50b95

Browse files
author
Felipe
committed
feat/Dashboard Chart with auto update
1 parent bb63fab commit cf50b95

File tree

14 files changed

+376
-2
lines changed

14 files changed

+376
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
class MissionControl::Jobs::DashboardController < MissionControl::Jobs::ApplicationController
2+
def index
3+
end
4+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
class MissionControl::Jobs::InternalApi::DashboardController < MissionControl::Jobs.base_controller_class.constantize
2+
include ActionView::Helpers::NumberHelper
3+
4+
def index
5+
render json: {
6+
uptime: {
7+
label: Time.now.strftime("%H:%M:%S"),
8+
pending: queue_job.pendings.where.not(id: failed_execution.select(:job_id)).size,
9+
failed: failed_execution.where("created_at >= ?", time_to_consult.seconds.ago).size,
10+
finished: queue_job.finisheds.where("finished_at >= ?", time_to_consult.seconds.ago).size,
11+
},
12+
total: {
13+
failed: number_with_delimiter(ActiveJob.jobs.failed.count),
14+
pending: number_with_delimiter(ActiveJob.jobs.pending.count),
15+
scheduled: number_with_delimiter(ActiveJob.jobs.scheduled.count),
16+
in_progress: number_with_delimiter(ActiveJob.jobs.in_progress.count),
17+
finished: number_with_delimiter(ActiveJob.jobs.finished.count)
18+
}
19+
},
20+
status: :ok
21+
end
22+
23+
private
24+
def time_to_consult
25+
params[:uptime].to_i || 5
26+
end
27+
28+
def failed_execution
29+
MissionControl::SolidQueueFailedExecution
30+
end
31+
32+
def queue_job
33+
MissionControl::SolidQueueJob
34+
end
35+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
class MissionControl::Jobs::InternalApi::NavigationController < MissionControl::Jobs::ApplicationController
2+
def index
3+
render partial: "layouts/mission_control/jobs/navigation_update", locals: {
4+
section: params[:section].to_sym
5+
}
6+
end
7+
end

app/helpers/mission_control/jobs/navigation_helper.rb

+4-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ module MissionControl::Jobs::NavigationHelper
22
attr_reader :page_title, :current_section
33

44
def navigation_sections
5-
{ queues: [ "Queues", application_queues_path(@application) ] }.tap do |sections|
5+
{ dashboard: [ "Dashboard", application_dashboard_index_path(@application) ] }.tap do |sections|
6+
sections[:queues] = [ "Queues", application_queues_path(@application) ]
7+
sections[:queues] = [ "Queues", application_queues_path(@application) ]
8+
69
supported_job_statuses.without(:pending).each do |status|
710
sections[navigation_section_for_status(status)] = [ "#{status.to_s.titleize} jobs (#{jobs_count_with_status(status)})", application_jobs_path(@application, status) ]
811
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
class MissionControl::SolidQueueFailedExecution < MissionControl::SolidQueueRecord
2+
self.table_name = 'solid_queue_failed_executions'
3+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class MissionControl::SolidQueueJob < MissionControl::SolidQueueRecord
2+
self.table_name = 'solid_queue_jobs'
3+
4+
scope :pendings, -> { where(finished_at: nil) }
5+
scope :finisheds, -> { where.not(finished_at: nil) }
6+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
class MissionControl::SolidQueueRecord < ApplicationRecord
2+
self.abstract_class = true
3+
4+
if !ActiveRecord::Base.connection.data_source_exists?('solid_queue_jobs')
5+
connects_to database: { writing: :queue, reading: :queue }
6+
end
7+
end
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<div class="tabs is-boxed">
1+
<div class="tabs is-boxed" id="navigation-sections">
22
<ul>
33
<% navigation_sections.each do |key, (label, url)| %>
44
<li class="<%= "is-active" if key == current_section %>">
@@ -7,3 +7,31 @@
77
<% end %>
88
</ul>
99
</div>
10+
11+
<script>
12+
document.addEventListener("turbo:load", () => {
13+
if (!window.Navigation || typeof window.Navigation.currentSection === 'undefined') {
14+
window.Navigation = {
15+
currentSection: "<%= @current_section %>",
16+
17+
changeSection(section) {
18+
this.currentSection = section;
19+
}
20+
};
21+
22+
setInterval(() => {
23+
fetch(`/jobs/applications/solidqueueusage/internal_api/navigation?section=${window.Navigation.currentSection}`)
24+
.then(response => response.text())
25+
.then(html => {
26+
const navigationSections = document.querySelector('#navigation-sections');
27+
if (navigationSections) {
28+
navigationSections.innerHTML = html;
29+
}
30+
})
31+
.catch(error => console.error("Error fetching navigation update:", error));
32+
}, 5000);
33+
} else {
34+
window.Navigation.currentSection = "<%= @current_section %>";
35+
}
36+
});
37+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<ul>
2+
<% navigation_sections.each do |key, (label, url)| %>
3+
<li class="<%= "is-active" if key == section %>">
4+
<%= link_to label, url %>
5+
</li>
6+
<% end %>
7+
</ul>

app/views/layouts/mission_control/jobs/application.html.erb

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
<meta name="viewport" content="width=device-width,initial-scale=1">
99
<meta name="turbo-cache-control" content="no-cache">
1010
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css">
11+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
12+
1113
<%= stylesheet_link_tag "mission_control/jobs/application", "data-turbo-track": "reload" %>
1214
<%= javascript_importmap_tags "application", importmap: MissionControl::Jobs.importmap %>
1315
</head>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
<% navigation(title: "Dashboard", section: :dashboard) %>
2+
3+
<div class="columns">
4+
<div class="column">
5+
<div class="notification">
6+
<h6 class="title is-6">Pending</h6>
7+
<span id="pending">--</span>
8+
</div>
9+
</div>
10+
<div class="column">
11+
<div class="notification">
12+
<h6 class="title is-6">Scheduled</h6>
13+
<span id="scheduled">--</span>
14+
</div>
15+
</div>
16+
<div class="column">
17+
<div class="notification">
18+
<h6 class="title is-6">In Progress</h6>
19+
<span id="in-progress">--</span>
20+
</div>
21+
</div>
22+
<div class="column">
23+
<div class="notification">
24+
<h6 class="title is-6">Finished</h6>
25+
<span id="finished">--</span>
26+
</div>
27+
</div>
28+
<div class="column">
29+
<div class="notification">
30+
<h6 class="title is-6">Failed</h6>
31+
<span id="failed">--</span>
32+
</div>
33+
</div>
34+
</div>
35+
36+
<div class="columns">
37+
<div class="column">
38+
General Overview
39+
</div>
40+
<div class="column has-text-right">
41+
<div class="select is-small">
42+
<select id="change-uptime" value="5" onchange="handleSelectUptime(this.value)">
43+
<option value="10">10 seconds</option>
44+
<option value="5">5 seconds</option>
45+
<option value="3">3 seconds</option>
46+
<option value="1">1 second</option>
47+
</select>
48+
</div>
49+
</div>
50+
</div>
51+
<hr/>
52+
53+
<div>
54+
<canvas id="general-overview-chart"></canvas>
55+
</div>
56+
57+
<script>
58+
59+
if (typeof uptimeInterval === "undefined") {
60+
var uptimeInterval = null;
61+
}
62+
if (typeof chart === "undefined") {
63+
var chart = null;
64+
}
65+
66+
document.addEventListener("turbo:load", () => {
67+
const canvas = document.getElementById('general-overview-chart');
68+
69+
if (!canvas)
70+
return;
71+
72+
const ctx = canvas.getContext('2d');
73+
74+
if (chart) {
75+
chart.destroy();
76+
chart = null;
77+
}
78+
79+
const finished = document.getElementById('finished');
80+
const scheduled = document.getElementById('scheduled');
81+
const pending = document.getElementById('pending');
82+
const inProgress = document.getElementById('in-progress');
83+
const failed = document.getElementById('failed');
84+
85+
let uptime = 5;
86+
87+
const labels = [];
88+
const data = {
89+
labels: labels,
90+
datasets: [{
91+
label: 'Success',
92+
data: [],
93+
fill: false,
94+
borderColor: 'rgb(0, 219, 124)',
95+
tension: 0.1
96+
},
97+
{
98+
label: 'Error',
99+
data: [],
100+
fill: false,
101+
borderColor: 'rgb(226, 15, 15)',
102+
tension: 0.1
103+
},
104+
{
105+
label: 'Pending',
106+
data: [],
107+
fill: false,
108+
borderColor: 'rgb(237, 209, 0)',
109+
tension: 0.1
110+
}]
111+
};
112+
113+
const config = {
114+
type: 'line',
115+
data: data,
116+
};
117+
118+
chart = new Chart(ctx, config);
119+
120+
function handleSelectUptime(value) {
121+
// console.log("Update Uptime to " + value);
122+
uptime = value;
123+
clearInterval(uptimeInterval);
124+
uptimeInterval = setInterval(() => updateChartData(), value * 1000);
125+
}
126+
127+
async function updateChartData() {
128+
try {
129+
const response = await fetch(`<%= application_internal_api_dashboard_index_path %>?uptime=${uptime}`);
130+
if (!response.ok) throw new Error('Network response was not ok');
131+
132+
const data = await response.json();
133+
134+
if (chart == null) return;
135+
136+
chart.data.labels.push(data.uptime.label);
137+
chart.data.labels = chart.data.labels.slice(-20);
138+
139+
AddChartData(0, data.uptime.finished);
140+
AddChartData(1, data.uptime.failed);
141+
AddChartData(2, data.uptime.pending);
142+
143+
finished.innerHTML = data.total.finished;
144+
inProgress.innerHTML = data.total.in_progress;
145+
pending.innerHTML = data.total.pending;
146+
scheduled.innerHTML = data.total.scheduled;
147+
failed.innerHTML = data.total.failed;
148+
149+
chart.update();
150+
} catch (error) {
151+
console.error('Error at consult chart API:', error);
152+
}
153+
}
154+
155+
function AddChartData(datasetIndex, quantity) {
156+
chart.data.datasets[datasetIndex].data.push(quantity);
157+
chart.data.datasets[datasetIndex].data = chart.data.datasets[datasetIndex].data.slice(-10);
158+
}
159+
160+
if (uptimeInterval != null)
161+
clearInterval(uptimeInterval);
162+
163+
uptimeInterval = setInterval(() => updateChartData(), 5000);
164+
updateChartData();
165+
166+
// Exponha a função no escopo global
167+
window.handleSelectUptime = handleSelectUptime;
168+
});
169+
170+
// Limpa o gráfico e o intervalo antes de sair da página
171+
document.addEventListener("turbo:before-render", () => {
172+
if (uptimeInterval != null) {
173+
clearInterval(uptimeInterval);
174+
uptimeInterval = null;
175+
}
176+
177+
if (chart) {
178+
chart.destroy();
179+
chart = null;
180+
}
181+
});
182+
</script>

config/routes.rb

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
MissionControl::Jobs::Engine.routes.draw do
22
resources :applications, only: [] do
3+
resources :dashboard, only: [ :index ]
34
resources :queues, only: [ :index, :show ] do
45
scope module: :queues do
56
resource :pause, only: [ :create, :destroy ]
@@ -17,6 +18,11 @@
1718
end
1819
end
1920

21+
namespace :internal_api do
22+
resources :dashboard, only: [ :index ]
23+
resources :navigation, only: [ :index ]
24+
end
25+
2026
resources :jobs, only: :index, path: ":status/jobs"
2127

2228
resources :workers, only: [ :index, :show ]

0 commit comments

Comments
 (0)