Skip to content

Commit

Permalink
feat/Dashboard Chart with auto update
Browse files Browse the repository at this point in the history
  • Loading branch information
Felipe committed Nov 10, 2024
1 parent bb63fab commit 839c0a4
Show file tree
Hide file tree
Showing 15 changed files with 402 additions and 3 deletions.
4 changes: 4 additions & 0 deletions app/controllers/mission_control/jobs/dashboard_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
class MissionControl::Jobs::DashboardController < MissionControl::Jobs::ApplicationController
def index
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
class MissionControl::Jobs::InternalApi::DashboardController < MissionControl::Jobs.base_controller_class.constantize
include ActionView::Helpers::NumberHelper

def index
render json: {
uptime: {
label: Time.now.strftime("%H:%M:%S"),
pending: MissionControl::SolidQueueJob.pendings.where.not(id: MissionControl::SolidQueueFailedExecution.select(:job_id)).size,
failed: MissionControl::SolidQueueFailedExecution.where("created_at >= ?", time_to_consult.seconds.ago).size,
finished: MissionControl::SolidQueueJob.finisheds.where("finished_at >= ?", time_to_consult.seconds.ago).size,
},
total: {
failed: number_with_delimiter(ActiveJob.jobs.failed.count),
pending: number_with_delimiter(ActiveJob.jobs.pending.count),
scheduled: number_with_delimiter(ActiveJob.jobs.scheduled.count),
in_progress: number_with_delimiter(ActiveJob.jobs.in_progress.count),
finished: number_with_delimiter(ActiveJob.jobs.finished.count)
}
},
status: :ok
end

private
def time_to_consult
params[:uptime] || 5
end

def humanize(count)
number_to_human(count,
format: "%n%u",
units: {
thousand: "K",
million: "M",
billion: "B",
trillion: "T",
quadrillion: "Q"
})
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class MissionControl::Jobs::InternalApi::NavigationController < MissionControl::Jobs::ApplicationController
def index
render partial: "layouts/mission_control/jobs/navigation_update", locals: {
section: params[:section].to_sym
}
end
end
5 changes: 4 additions & 1 deletion app/helpers/mission_control/jobs/navigation_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ module MissionControl::Jobs::NavigationHelper
attr_reader :page_title, :current_section

def navigation_sections
{ queues: [ "Queues", application_queues_path(@application) ] }.tap do |sections|
{ dashboard: [ "Dashboard", application_dashboard_index_path(@application) ] }.tap do |sections|
sections[:queues] = [ "Queues", application_queues_path(@application) ]
sections[:queues] = [ "Queues", application_queues_path(@application) ]

supported_job_statuses.without(:pending).each do |status|
sections[navigation_section_for_status(status)] = [ "#{status.to_s.titleize} jobs (#{jobs_count_with_status(status)})", application_jobs_path(@application, status) ]
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ const application = Application.start()
application.debug = false
window.Stimulus = application

export { application }
export { application }
3 changes: 3 additions & 0 deletions app/models/mission_control/solid_queue_failed_execution.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class MissionControl::SolidQueueFailedExecution < MissionControl::SolidQueueRecord
self.table_name = 'solid_queue_failed_executions'
end
6 changes: 6 additions & 0 deletions app/models/mission_control/solid_queue_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class MissionControl::SolidQueueJob < MissionControl::SolidQueueRecord
self.table_name = 'solid_queue_jobs'

scope :pendings, -> { where(finished_at: nil) }
scope :finisheds, -> { where.not(finished_at: nil) }
end
7 changes: 7 additions & 0 deletions app/models/mission_control/solid_queue_record.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class MissionControl::SolidQueueRecord < ApplicationRecord
self.abstract_class = true

if !ActiveRecord::Base.connection.data_source_exists?('solid_queue_jobs')
connects_to database: { writing: :queue, reading: :queue }
end
end
30 changes: 29 additions & 1 deletion app/views/layouts/mission_control/jobs/_navigation.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class="tabs is-boxed">
<div class="tabs is-boxed" id="navigation-sections">
<ul>
<% navigation_sections.each do |key, (label, url)| %>
<li class="<%= "is-active" if key == current_section %>">
Expand All @@ -7,3 +7,31 @@
<% end %>
</ul>
</div>

<script>
document.addEventListener("turbo:load", () => {
if (!window.Navigation || typeof window.Navigation.currentSection === 'undefined') {
window.Navigation = {
currentSection: "<%= @current_section %>",

changeSection(section) {
this.currentSection = section;
}
};

setInterval(() => {
fetch(`/jobs/applications/solidqueueusage/internal_api/navigation?section=${window.Navigation.currentSection}`)
.then(response => response.text())
.then(html => {
const navigationSections = document.querySelector('#navigation-sections');
if (navigationSections) {
navigationSections.innerHTML = html;
}
})
.catch(error => console.error("Error fetching navigation update:", error));
}, 5000);
} else {
window.Navigation.currentSection = "<%= @current_section %>";
}
});
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<ul>
<% navigation_sections.each do |key, (label, url)| %>
<li class="<%= "is-active" if key == section %>">
<%= link_to label, url %>
</li>
<% end %>
</ul>
2 changes: 2 additions & 0 deletions app/views/layouts/mission_control/jobs/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="turbo-cache-control" content="no-cache">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.1/css/bulma.min.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

<%= stylesheet_link_tag "mission_control/jobs/application", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags "application", importmap: MissionControl::Jobs.importmap %>
</head>
Expand Down
184 changes: 184 additions & 0 deletions app/views/mission_control/jobs/dashboard/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
<% navigation(title: "Dashboard", section: :dashboard) %>

<div class="columns">
<div class="column">
<div class="notification">
<h6 class="title is-6">Pending</h6>
<span id="pending">--</span>
</div>
</div>
<div class="column">
<div class="notification">
<h6 class="title is-6">Scheduled</h6>
<span id="scheduled">--</span>
</div>
</div>
<div class="column">
<div class="notification">
<h6 class="title is-6">In Progress</h6>
<span id="in-progress">--</span>
</div>
</div>
<div class="column">
<div class="notification">
<h6 class="title is-6">Finished</h6>
<span id="finished">--</span>
</div>
</div>
<div class="column">
<div class="notification">
<h6 class="title is-6">Failed</h6>
<span id="failed">--</span>
</div>
</div>
</div>

<div class="columns">
<div class="column">
General Overview
</div>
<div class="column has-text-right">
<div class="select is-small">
<select id="change-uptime" value="5" onchange="handleSelectUptime(this.value)">
<option value="10">10 seconds</option>
<option value="5">5 seconds</option>
<option value="3">3 seconds</option>
<option value="1">1 second</option>
</select>
</div>
</div>
</div>
<hr/>

<div>
<canvas id="general-overview-chart" style="max-height: 320px"></canvas>
</div>

<script>

if (typeof uptimeInterval === "undefined") {
var uptimeInterval = null;
}
if (typeof chart === "undefined") {
var chart = null;
}

document.addEventListener("turbo:load", () => {
const canvas = document.getElementById('general-overview-chart');

if (!canvas)
return;

const ctx = canvas.getContext('2d');

if (chart) {
chart.destroy();
chart = null;
}

const finished = document.getElementById('finished');
const scheduled = document.getElementById('scheduled');
const pending = document.getElementById('pending');
const inProgress = document.getElementById('in-progress');
const failed = document.getElementById('failed');

let uptime = 5;

const labels = [];
const data = {
labels: labels,
datasets: [{
label: 'Success',
data: [],
fill: false,
borderColor: 'rgb(0, 219, 124)',
tension: 0.1
},
{
label: 'Error',
data: [],
fill: false,
borderColor: 'rgb(226, 15, 15)',
tension: 0.1
},
{
label: 'Pending',
data: [],
fill: false,
borderColor: 'rgb(237, 209, 0)',
tension: 0.1
}]
};

const config = {
type: 'line',
data: data,
};

chart = new Chart(ctx, config);

function handleSelectUptime(value) {
// console.log("Update Uptime to " + value);
uptime = value;
clearInterval(uptimeInterval);
uptimeInterval = setInterval(() => updateChartData(), value * 1000);
}

async function updateChartData() {
try {
const response = await fetch(`<%= application_internal_api_dashboard_index_path %>?uptime=${uptime}`);
if (!response.ok) throw new Error('Network response was not ok');

const data = await response.json();

if (chart == null) return;

chart.data.labels.push(data.uptime.label);
chart.data.labels = chart.data.labels.slice(-10);

AddChartData(0, data.uptime.finished);
AddChartData(1, data.uptime.failed);
AddChartData(2, data.uptime.pending);

finished.innerHTML = data.total.finished;
inProgress.innerHTML = data.total.in_progress;
pending.innerHTML = data.total.pending;
scheduled.innerHTML = data.total.scheduled;
failed.innerHTML = data.total.failed;

chart.update();
} catch (error) {
console.error('Error at consult chart API:', error);
}
}

function AddChartData(datasetIndex, quantity) {
chart.data.datasets[datasetIndex].data.push(quantity);
chart.data.datasets[datasetIndex].data = chart.data.datasets[datasetIndex].data.slice(-10);
}

if (uptimeInterval != null)
clearInterval(uptimeInterval);

uptimeInterval = setInterval(() => updateChartData(), 5000);
updateChartData();

// Exponha a função no escopo global
window.handleSelectUptime = handleSelectUptime;
});

// Limpa o gráfico e o intervalo antes de sair da página
document.addEventListener("turbo:before-render", () => {
if (uptimeInterval != null) {
clearInterval(uptimeInterval);
uptimeInterval = null;
}

if (chart) {
chart.destroy();
chart = null;
}
});


</script>
6 changes: 6 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
MissionControl::Jobs::Engine.routes.draw do
resources :applications, only: [] do
resources :dashboard, only: [ :index ]
resources :queues, only: [ :index, :show ] do
scope module: :queues do
resource :pause, only: [ :create, :destroy ]
Expand All @@ -17,6 +18,11 @@
end
end

namespace :internal_api do
resources :dashboard, only: [ :index ]
resources :navigation, only: [ :index ]
end

resources :jobs, only: :index, path: ":status/jobs"

resources :workers, only: [ :index, :show ]
Expand Down
15 changes: 15 additions & 0 deletions lib/active_job/jobs_relation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,21 @@ def with_status(status)
end
end

def with_batata

puts "================"
puts "With Batata:"

result = ActiveRecord::Base.connection.execute("SELECT count(*) FROM solid_queue_jobs WHERE finished_at IS NOT NULL")

puts result

puts "================"

# time_threshold = Time.now - 5.seconds
# clone_with status: 'pending' # time_threshold..Time.now
end

STATUSES.each do |status|
define_method status do
with_status(status)
Expand Down
Loading

0 comments on commit 839c0a4

Please sign in to comment.