add dual-axis graph for dns checks (#74)
DNS check graph now shows two datasets: Response Time (ms) on left y-axis and Status (Up/Down) on right y-axis with stepped green fill. Also adds space before units in chart labels. Reviewed-on: graham/towerops-web#74
This commit is contained in:
parent
272d4bcdc9
commit
9d0c824aa5
4 changed files with 123 additions and 31 deletions
|
|
@ -100,6 +100,7 @@ const SensorChart: SensorChartHook = {
|
|||
const unit = this.el.dataset.unit ?? '%'
|
||||
const autoScale = this.el.dataset.autoScale === 'true' || this.el.dataset.autoScale === 'true'
|
||||
const showZeroLine = this.el.dataset.showZeroLine === 'true' || this.el.dataset.showZeroLine === 'true'
|
||||
const dualAxis = this.el.dataset.dualAxis === 'true'
|
||||
const range = this.el.dataset.range || '24h'
|
||||
const isTrafficChart = unit === 'bps'
|
||||
|
||||
|
|
@ -127,11 +128,24 @@ const SensorChart: SensorChartHook = {
|
|||
pointHoverRadius: 4,
|
||||
}
|
||||
|
||||
// Add fill for traffic charts to create area effect
|
||||
if (isTrafficChart) {
|
||||
baseConfig.fill = 'origin' // Fill to the zero line
|
||||
// Preserve yAxisID from server data for dual-axis charts
|
||||
if ((dataset as any).yAxisID) {
|
||||
(baseConfig as any).yAxisID = (dataset as any).yAxisID
|
||||
}
|
||||
|
||||
// Status dataset (dual axis, right side) uses stepped line with fill
|
||||
if (dualAxis && (dataset as any).yAxisID === 'y1') {
|
||||
(baseConfig as any).stepped = 'before'
|
||||
baseConfig.tension = 0
|
||||
baseConfig.fill = 'origin'
|
||||
baseConfig.borderWidth = 1
|
||||
// Use green with low opacity for the status fill area
|
||||
baseConfig.borderColor = 'rgb(34, 197, 94)'
|
||||
baseConfig.backgroundColor = 'rgba(34, 197, 94, 0.1)'
|
||||
} else if (isTrafficChart) {
|
||||
baseConfig.fill = 'origin'
|
||||
} else {
|
||||
baseConfig.fill = false // No fill for CPU/memory/storage charts
|
||||
baseConfig.fill = false
|
||||
}
|
||||
|
||||
return baseConfig
|
||||
|
|
@ -206,7 +220,7 @@ const SensorChart: SensorChartHook = {
|
|||
if (unit === '') {
|
||||
return Math.round(value)
|
||||
}
|
||||
return value + unit
|
||||
return value + ' ' + unit
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
|
|
@ -340,20 +354,40 @@ const SensorChart: SensorChartHook = {
|
|||
})
|
||||
},
|
||||
label: function(context: any) {
|
||||
// Status dataset on dual-axis chart
|
||||
if (dualAxis && context.dataset.yAxisID === 'y1') {
|
||||
return context.dataset.label + ': ' + (context.parsed.y === 1 ? 'Up' : 'Down')
|
||||
}
|
||||
if (unit === 'bps') {
|
||||
return context.dataset.label + ': ' + formatBps(context.parsed.y)
|
||||
}
|
||||
if (unit === '') {
|
||||
return context.dataset.label + ': ' + Math.round(context.parsed.y)
|
||||
}
|
||||
return context.dataset.label + ': ' + context.parsed.y.toFixed(1) + unit
|
||||
return context.dataset.label + ': ' + context.parsed.y.toFixed(1) + ' ' + unit
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: xAxisConfig,
|
||||
y: yAxisConfig
|
||||
y: yAxisConfig,
|
||||
...(dualAxis ? {
|
||||
y1: {
|
||||
position: 'right' as const,
|
||||
min: -0.1,
|
||||
max: 1.1,
|
||||
grid: { drawOnChartArea: false },
|
||||
ticks: {
|
||||
stepSize: 1,
|
||||
callback: function(value: number) {
|
||||
if (value === 1) return 'Up'
|
||||
if (value === 0) return 'Down'
|
||||
return ''
|
||||
}
|
||||
}
|
||||
}
|
||||
} : {})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -255,6 +255,24 @@ defmodule Towerops.Monitoring do
|
|||
Enum.sort_by(historical_results ++ recent_results, & &1.timestamp, DateTime)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets graph data for a check including status field.
|
||||
|
||||
Returns list of %{timestamp: DateTime.t(), value: float() | nil, status: integer()} sorted by timestamp.
|
||||
Used for DNS checks that need to show both availability and response time.
|
||||
"""
|
||||
def get_check_graph_data_with_status(check_id, from_time, to_time) do
|
||||
Repo.all(
|
||||
from(r in CheckResult,
|
||||
where: r.check_id == ^check_id,
|
||||
where: r.checked_at >= ^from_time,
|
||||
where: r.checked_at <= ^to_time,
|
||||
order_by: [asc: r.checked_at],
|
||||
select: %{timestamp: r.checked_at, value: r.value, status: r.status}
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates check state after a check execution.
|
||||
Handles soft/hard state transitions.
|
||||
|
|
|
|||
|
|
@ -110,12 +110,68 @@ defmodule ToweropsWeb.GraphLive.Show do
|
|||
|
||||
defp initialize_check_graph_view(socket, device, check, params) do
|
||||
range = Map.get(params, "range", "24h")
|
||||
|
||||
# Load graph data for the check
|
||||
{from_time, to_time} = get_time_range_for_graph(range)
|
||||
graph_data_points = Monitoring.get_check_graph_data(check.id, from_time, to_time)
|
||||
|
||||
# Convert to chart format
|
||||
{chart_data, unit, auto_scale, dual_axis} = build_check_chart_data(check, from_time, to_time)
|
||||
{max_value, min_value} = calculate_chart_stats(chart_data)
|
||||
|
||||
socket
|
||||
|> assign(:device, device)
|
||||
|> assign(:device_id, device.id)
|
||||
|> assign(:check, check)
|
||||
|> assign(:check_id, check.id)
|
||||
|> assign(:range, range)
|
||||
|> assign(:page_title, "#{device.name} - #{check.name}")
|
||||
|> assign(:chart_title, check.name)
|
||||
|> assign(:chart_data, chart_data)
|
||||
|> assign(:unit, unit)
|
||||
|> assign(:auto_scale, auto_scale)
|
||||
|> assign(:dual_axis, dual_axis)
|
||||
|> assign(:show_zero_line, false)
|
||||
|> assign(:max_value, max_value)
|
||||
|> assign(:min_value, min_value)
|
||||
|> assign(:is_live_mode, false)
|
||||
|> assign(:has_agent_assignment, false)
|
||||
end
|
||||
|
||||
defp build_check_chart_data(%{check_type: "dns"} = check, from_time, to_time) do
|
||||
points = Monitoring.get_check_graph_data_with_status(check.id, from_time, to_time)
|
||||
|
||||
if Enum.empty?(points) do
|
||||
{nil, "ms", true, false}
|
||||
else
|
||||
response_time_dataset = %{
|
||||
label: "Response Time (ms)",
|
||||
yAxisID: "y",
|
||||
data:
|
||||
points
|
||||
|> Enum.filter(fn p -> p.value != nil end)
|
||||
|> Enum.map(fn p ->
|
||||
%{x: DateTime.to_unix(p.timestamp, :millisecond), y: Float.round(p.value, 1)}
|
||||
end)
|
||||
}
|
||||
|
||||
status_dataset = %{
|
||||
label: "Status",
|
||||
yAxisID: "y1",
|
||||
data:
|
||||
Enum.map(points, fn p ->
|
||||
%{
|
||||
x: DateTime.to_unix(p.timestamp, :millisecond),
|
||||
y: if(p.status == 0, do: 1, else: 0)
|
||||
}
|
||||
end)
|
||||
}
|
||||
|
||||
chart_data = Jason.encode!(%{datasets: [response_time_dataset, status_dataset]})
|
||||
{chart_data, "ms", true, true}
|
||||
end
|
||||
end
|
||||
|
||||
defp build_check_chart_data(check, from_time, to_time) do
|
||||
graph_data_points = Monitoring.get_check_graph_data(check.id, from_time, to_time)
|
||||
{unit, auto_scale} = get_check_unit_and_scale(check)
|
||||
|
||||
chart_data =
|
||||
if Enum.empty?(graph_data_points) do
|
||||
nil
|
||||
|
|
@ -134,26 +190,7 @@ defmodule ToweropsWeb.GraphLive.Show do
|
|||
Jason.encode!(%{datasets: [dataset]})
|
||||
end
|
||||
|
||||
# Get unit and title based on check type and config
|
||||
{unit, auto_scale} = get_check_unit_and_scale(check)
|
||||
{max_value, min_value} = calculate_chart_stats(chart_data)
|
||||
|
||||
socket
|
||||
|> assign(:device, device)
|
||||
|> assign(:device_id, device.id)
|
||||
|> assign(:check, check)
|
||||
|> assign(:check_id, check.id)
|
||||
|> assign(:range, range)
|
||||
|> assign(:page_title, "#{device.name} - #{check.name}")
|
||||
|> assign(:chart_title, check.name)
|
||||
|> assign(:chart_data, chart_data)
|
||||
|> assign(:unit, unit)
|
||||
|> assign(:auto_scale, auto_scale)
|
||||
|> assign(:show_zero_line, false)
|
||||
|> assign(:max_value, max_value)
|
||||
|> assign(:min_value, min_value)
|
||||
|> assign(:is_live_mode, false)
|
||||
|> assign(:has_agent_assignment, false)
|
||||
{chart_data, unit, auto_scale, false}
|
||||
end
|
||||
|
||||
defp get_time_range_for_graph("1h"), do: {DateTime.add(DateTime.utc_now(), -1, :hour), DateTime.utc_now()}
|
||||
|
|
@ -284,6 +321,7 @@ defmodule ToweropsWeb.GraphLive.Show do
|
|||
|> assign(:chart_data, chart_data)
|
||||
|> assign(:unit, unit)
|
||||
|> assign(:auto_scale, auto_scale)
|
||||
|> assign(:dual_axis, false)
|
||||
|> assign(:show_zero_line, show_zero_line)
|
||||
|> assign(:max_value, max_value)
|
||||
|> assign(:min_value, min_value)
|
||||
|
|
@ -306,6 +344,7 @@ defmodule ToweropsWeb.GraphLive.Show do
|
|||
|> assign(:chart_data, Jason.encode!(%{datasets: []}))
|
||||
|> assign(:unit, unit)
|
||||
|> assign(:auto_scale, auto_scale)
|
||||
|> assign(:dual_axis, false)
|
||||
|> assign(:show_zero_line, sensor_type == "traffic")
|
||||
|> assign(:max_value, nil)
|
||||
|> assign(:min_value, nil)
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@
|
|||
data-unit={@unit}
|
||||
data-auto-scale={to_string(@auto_scale)}
|
||||
data-show-zero-line={to_string(@show_zero_line)}
|
||||
data-dual-axis={to_string(@dual_axis)}
|
||||
data-range={@range}
|
||||
style="height: 600px;"
|
||||
>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue