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

Add support for live polling the dashboard #528

Merged
merged 9 commits into from
Feb 27, 2022
126 changes: 125 additions & 1 deletion engine/app/assets/scripts.js
Original file line number Diff line number Diff line change
@@ -1 +1,125 @@
GoodJob = {};
GoodJob = {
// Register functions to execute when the DOM is ready
ready: (callback) => {
if (document.readyState != "loading"){
callback()
} else {
document.addEventListener("DOMContentLoaded", callback)
}
},

init: () => {
GoodJob.updateSettings()
GoodJob.addListeners()
GoodJob.pollUpdates()
GoodJob.renderCharts(true)
},

addListeners() {
const gjActionEls = document.querySelectorAll('[data-gj-action]')

for (let i = 0; i < gjActionEls.length; i++) {
const el = gjActionEls[i]
const [eventName, func] = el.dataset.gjAction.split('#')

el.addEventListener(eventName, GoodJob[func])
}
},

updateSettings() {
const queryString = window.location.search
const urlParams = new URLSearchParams(queryString)

// livepoll interval and enablement
if (urlParams.has('poll')) {
GoodJob.pollEnabled = true
GoodJob.pollInterval = parseInt(urlParams.get('poll'))
GoodJob.setStorage('pollInterval', GoodJob.pollInterval)
} else {
GoodJob.pollEnabled = GoodJob.getStorage('pollEnabled') || false
GoodJob.pollInterval = GoodJob.getStorage('pollInterval') || 5000 // default 5sec
if (GoodJob.pollEnabled) {
// Update the UI element
document.getElementById('toggle-poll').checked = true
}
}
},

togglePoll: (ev) => {
GoodJob.pollEnabled = ev.currentTarget.checked
GoodJob.setStorage('pollEnabled', GoodJob.pollEnabled)
},

pollUpdates: () => {
setTimeout(() => {
if (GoodJob.pollEnabled == true) {
fetch(window.location.href)
.then(resp => resp.text())
.then(GoodJob.updateContent)
.finally(GoodJob.pollUpdates)
} else {
GoodJob.pollUpdates()
}
}, GoodJob.pollInterval)
},

updateContent: (newContent) => {
const domParser = new DOMParser()
const parsedDOM = domParser.parseFromString(newContent, "text/html")

const newElements = parsedDOM.querySelectorAll('[data-gj-poll-replace]')

for (let i = 0; i < newElements.length; i++) {
const newEl = newElements[i]
const oldEl = document.getElementById(newEl.id)

if (oldEl) {
oldEl.replaceWith(newEl)
}
}

GoodJob.renderCharts(false)
},

renderCharts: (animate) => {
const charts = document.querySelectorAll('.chart')

for (let i = 0; i < charts.length; i++) {
const chartEl = charts[i]
const chartData = JSON.parse(chartEl.dataset.json)

const ctx = chartEl.getContext('2d');
const chart = new Chart(ctx, {
type: 'line',
data: {
labels: chartData.labels,
datasets: chartData.datasets
},
options: {
animation: animate,
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
}
});
}
},

getStorage: (key) => {
const value = localStorage.getItem('goodjob-' + key)

if (value === 'true') { return true }
else if (value === 'false') { return false }
else { return value }
},

setStorage: (key, value) => {
localStorage.setItem('goodjob-' + key, value)
}
};

GoodJob.ready(GoodJob.init)
2 changes: 1 addition & 1 deletion engine/app/views/good_job/executions/_table.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class="card my-3">
<div class="card my-3" data-gj-poll-replace id="executions-table">
<div class="table-responsive">
<table class="table card-table table-bordered table-hover table-sm mb-0" id="executions_index_table">
<thead>
Expand Down
4 changes: 2 additions & 2 deletions engine/app/views/good_job/executions/index.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class="card my-3 p-6">
<div class="card my-3 p-6" data-gj-poll-replace id="executions-chart">
<%= render 'good_job/shared/chart', chart_data: GoodJob::ScheduledByQueueChart.new(@filter).data %>
</div>

Expand All @@ -7,7 +7,7 @@
<%= render 'good_job/executions/table', executions: @filter.records %>

<% if @filter.records.present? %>
<nav aria-label="Job pagination" class="mt-3">
<nav aria-label="Job pagination" class="mt-3" data-gj-poll-replace id="executions-pagination">
<ul class="pagination">
<li class="page-item">
<%= link_to({ after_scheduled_at: (@filter.last.scheduled_at || @filter.last.created_at), after_id: @filter.last.id }, class: "page-link") do %>
Expand Down
2 changes: 1 addition & 1 deletion engine/app/views/good_job/jobs/_table.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class="card my-3">
<div class="card my-3" data-gj-poll-replace id="jobs-table">
<div class="table-responsive">
<table class="table card-table table-bordered table-hover table-sm mb-0">
<thead>
Expand Down
4 changes: 2 additions & 2 deletions engine/app/views/good_job/jobs/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<h2>All Jobs</h2>
</div>

<div class="card my-3 p-6">
<div class="card my-3 p-6" data-gj-poll-replace id="jobs-chart">
<%= render 'good_job/shared/chart', chart_data: GoodJob::ScheduledByQueueChart.new(@filter).data %>
</div>

Expand All @@ -11,7 +11,7 @@
<%= render 'good_job/jobs/table', jobs: @filter.records %>

<% if @filter.records.present? %>
<nav aria-label="Job pagination" class="mt-3">
<nav aria-label="Job pagination" class="mt-3" data-gj-poll-replace id="jobs-pagination">
<ul class="pagination">
<li class="page-item">
<%= link_to(@filter.to_params(after_scheduled_at: (@filter.last.scheduled_at || @filter.last.created_at), after_id: @filter.last.id), class: "page-link") do %>
Expand Down
74 changes: 38 additions & 36 deletions engine/app/views/good_job/processes/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,45 @@
<h2>Processes</h2>
</div>

<% if !GoodJob::Process.migrated? %>
<div class="card my-3">
<div class="card-body">
<p class="card-text">
<em>Feature unavailable because of pending database migration.</em>
</p>
<div data-gj-poll-replace id="processes">
<% if !GoodJob::Process.migrated? %>
<div class="card my-3">
<div class="card-body">
<p class="card-text">
<em>Feature unavailable because of pending database migration.</em>
</p>
</div>
</div>
</div>
<% elsif @processes.present? %>
<div class="card my-3">
<div class="table-responsive">
<table class="table card-table table-bordered table-hover table-sm mb-0">
<thead>
<tr>
<th>Process UUID</th>
<th>Created At</th></th>
<th>State</th>
</tr>
</thead>
<tbody>
<% @processes.each do |process| %>
<tr class="<%= dom_class(process) %>" id="<%= dom_id(process) %>">
<td><%= process.id %></td>
<td><%= relative_time(process.created_at) %></td>
<td><%= tag.pre JSON.pretty_generate(process.state) %></td>
<% elsif @processes.present? %>
<div class="card my-3">
<div class="table-responsive">
<table class="table card-table table-bordered table-hover table-sm mb-0">
<thead>
<tr>
<th>Process UUID</th>
<th>Created At</th></th>
<th>State</th>
</tr>
<% end %>
</tbody>
</table>
</thead>
<tbody>
<% @processes.each do |process| %>
<tr class="<%= dom_class(process) %>" id="<%= dom_id(process) %>">
<td><%= process.id %></td>
<td><%= relative_time(process.created_at) %></td>
<td><%= tag.pre JSON.pretty_generate(process.state) %></td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
<% else %>
<div class="card my-3">
<div class="card-body">
<p class="card-text">
<em>No GoodJob processes found.</em>
</p>
<% else %>
<div class="card my-3">
<div class="card-body">
<p class="card-text">
<em>No GoodJob processes found.</em>
</p>
</div>
</div>
</div>
<% end %>
<% end %>
</div>
24 changes: 1 addition & 23 deletions engine/app/views/good_job/shared/_chart.erb
Original file line number Diff line number Diff line change
@@ -1,25 +1,3 @@
<div class="chart-wrapper">
<canvas id="chart"></canvas>
<canvas class="chart" data-json="<%= chart_data.to_json %>"></canvas>
</div>

<%= javascript_tag nonce: true do %>
const chartData = <%== chart_data.to_json %>;

const ctx = document.getElementById('chart').getContext('2d');
const chart = new Chart(ctx, {
type: 'line',
data: {
labels: chartData.labels,
datasets: chartData.datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
}
});
<% end %>
3 changes: 2 additions & 1 deletion engine/app/views/good_job/shared/_filter.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<%= form_with(url: "", method: :get, local: true, id: "filter_form") do |form| %>
<%= hidden_field_tag :poll, value: params[:poll] %>
<div class="d-flex flex-row w-100">
<div class="me-2">
<label for="job_class_filter">Job class</label>
Expand Down Expand Up @@ -55,4 +56,4 @@
});
})
})
<% end %>
<% end %>
22 changes: 21 additions & 1 deletion engine/app/views/layouts/good_job/base.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@
</div>
</li>
</ul>
<div class="text-muted" title="Now is <%= Time.current %>">Times are displayed in <%= Time.current.zone %> timezone</div>
<div>
<input type="checkbox" id="toggle-poll" name="toggle-poll" data-gj-action='change#togglePoll' <%= 'checked' if params[:poll].present? %>>
<label for="toggle-poll">Live Poll</label>
</div>
</div>
</div>
</nav>
Expand All @@ -70,7 +73,24 @@
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<% end %>

<%= yield %>
</div>

<footer class="footer mt-auto py-3 bg-light fixed-bottom" id="footer" data-gj-poll-replace>
<div class="container-fluid">
<div class="row">
<div class="col-6">
<span class="text-muted" title="Now is <%= Time.current %>">
Last updated: <%= Time.current %>
</span>
</div>

<div class="col-6 text-end">
Remember, you're doing a Good Job too!
</div>
</div>
</div>
</footer>
</body>
</html>