Files
community-plugins/link-ip-monitor/panel.luau
T
NilsonlinuxandGitHub af32a36f1c Add nilsonlinux/link-ip-monitor v1.0.0 (#320)
* Add nilsonlinux/link-ip-monitor v1.0.0

* Add nilsonlinux/link-ip-monitor v1.0.0

* Update plugin ID in README.md

* Rename project to link-ip-monitor and update ID

Updated the project name and ID in the README.

* Update plugin ID and command syntax in README
2026-08-09 13:50:32 -04:00

360 lines
10 KiB
Luau

--!nonstrict
-- panel.luau
-- Window shown when clicking the bar widget: a form to add an IP,
-- hostname or link (with an optional description field below it), and
-- the list of monitored hosts — each with its label (if set), a status
-- pill, response time in ms (when online), a drag handle to reorder, and
-- a remove button (with confirmation).
local ips = {} -- { {ip=, label=, status=, latency_ms=}, ... }
local draftIp = ""
local draftLabel = ""
local draftKey = 0
local draftError = ""
local pendingDelete = nil
local showAddForm = false
local DRAG_TYPE = "link-ip-monitor-host"
local STATUS_COLORS = {
up = { fill = "#2e7d32", text = "#ffffff" },
down = { fill = "#c62828", text = "#ffffff" },
checking = { fill = "#5f6368", text = "#ffffff" },
}
local render
-- Accepts an IP, a plain hostname, or a link (http://..., https://...)
-- and returns just the host — scheme, port, path and query are dropped.
-- Same logic as service.luau (duplicated here just to give immediate
-- feedback in the field).
local function extractHost(raw)
local s = noctalia.string.trim(raw or "")
s = s:gsub("^%a[%w+.-]*://", "")
s = s:match("^([^/%?#]+)") or s
s = s:match("^([^:]+)") or s
return s
end
local function isValidIPv4(host)
local a, b, c, d = host:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)$")
if not a then
return false
end
for _, n in ipairs({ a, b, c, d }) do
local num = tonumber(n)
if num == nil or num < 0 or num > 255 then
return false
end
end
return true
end
local function isValidHostname(host)
if host == "" or #host > 253 then
return false
end
local labelCount = 0
for label in host:gmatch("[^%.]+") do
labelCount = labelCount + 1
if #label == 0 or #label > 63 then
return false
end
if not label:match("^%w[%w%-]*%w$") and not label:match("^%w$") then
return false
end
end
return labelCount > 0
end
local function normalizeHost(raw)
local host = extractHost(raw)
if host == "" then
return nil
end
if isValidIPv4(host) or isValidHostname(host) then
return host
end
return nil
end
local STATUS_TR_KEY = {
up = "panel.status_online",
down = "panel.status_offline",
checking = "panel.status_checking",
}
local function pill(status)
local colors = STATUS_COLORS[status] or STATUS_COLORS.checking
local label = noctalia.tr(STATUS_TR_KEY[status] or "panel.status_checking")
return ui.row({
fill = colors.fill,
radius = 8,
paddingH = 8,
paddingV = 2,
align = "center",
justify = "center",
}, {
ui.label({ text = label, fontSize = 11, fontWeight = "bold", color = colors.text }),
})
end
-- Thin strip between rows (and before the first / after the last) that
-- accepts a dropped host, indicating where it will land.
local function insertionZone(index)
return ui.dropZone({
key = "gap-" .. index,
accepts = { DRAG_TYPE },
value = tostring(index),
onDrop = "onIpDropped",
height = 3,
radius = 4,
expandOnDrag = true,
hitSlop = 24,
})
end
function onIpDropped(payload, value)
local insertAt = tonumber(value)
if type(payload) ~= "string" or payload == "" or insertAt == nil then
return
end
noctalia.state.set("link_ip_monitor.cmd", { op = "reorder", ip = payload, index = insertAt })
end
local function hostRow(item)
local id = item.ip
local deleting = pendingDelete == id
local actions
if deleting then
actions = ui.row({ gap = 4, align = "center" }, {
ui.button({
glyph = "check",
variant = "destructive",
controlSize = "sm",
tooltip = noctalia.tr("panel.tooltip_confirm_remove"),
onClick = function()
noctalia.state.set("link_ip_monitor.cmd", { op = "remove", ip = id })
pendingDelete = nil
render()
end,
}),
ui.button({
glyph = "close",
variant = "ghost",
controlSize = "sm",
tooltip = noctalia.tr("panel.tooltip_cancel"),
onClick = function()
pendingDelete = nil
render()
end,
}),
})
else
actions = ui.button({
glyph = "trash",
variant = "ghost",
controlSize = "sm",
tooltip = noctalia.tr("panel.tooltip_remove"),
onClick = function()
pendingDelete = id
render()
end,
})
end
-- Left column: label (if set) in bold + host as a caption; without a
-- label, the host becomes the main text.
local infoChildren = {}
local hasLabel = item.label ~= nil and item.label ~= ""
if hasLabel then
table.insert(infoChildren, ui.label({ text = item.label, fontWeight = "bold", fontSize = 13 }))
table.insert(infoChildren, ui.label({ text = item.ip, color = "on_surface_variant", fontSize = 10 }))
else
table.insert(infoChildren, ui.label({ text = item.ip, fontWeight = "bold", fontSize = 13 }))
end
-- Right column: status pill + response time in ms (only while online
-- and when the measurement exists).
local statusChildren = { pill(item.status) }
if item.status == "up" and item.latency_ms ~= nil then
table.insert(statusChildren, ui.label({
text = tostring(math.floor(item.latency_ms + 0.5)) .. " ms",
fontSize = 10,
color = "on_surface_variant",
}))
end
return ui.row({
key = "ip-" .. id,
gap = 8,
padding = 10,
radius = 8,
fill = "surface_variant/0.35",
align = "center",
justify = "space_between",
}, {
ui.dragSource({
key = "grip-" .. id,
dragType = DRAG_TYPE,
payload = id,
previewAncestor = 1,
liftFromLayout = true,
width = 20,
height = 20,
align = "center",
justify = "center",
tooltip = noctalia.tr("panel.tooltip_drag"),
}, {
ui.glyph({ name = "menu-2", size = 14, color = "on_surface_variant" }),
}),
ui.column({ flexGrow = 1, gap = 1 }, infoChildren),
ui.column({ align = "end", gap = 2 }, statusChildren),
actions,
})
end
local function emptyState()
return ui.column({ gap = 10, align = "center", padding = 32, flexGrow = 1, justify = "center" }, {
ui.glyph({ name = "activity", size = 32, color = "outline" }),
ui.label({ text = noctalia.tr("panel.empty"), color = "outline" }),
})
end
render = function()
local body
if #ips == 0 then
body = emptyState()
else
local rows = {}
for i, item in ipairs(ips) do
table.insert(rows, insertionZone(i))
table.insert(rows, hostRow(item))
end
table.insert(rows, insertionZone(#ips + 1))
body = ui.scroll({ gap = 6, flexGrow = 1 }, rows)
end
local column = {}
table.insert(column, ui.row({ align = "center", justify = "space_between" }, {
ui.label({ text = noctalia.tr("title"), fontSize = 16, fontWeight = "bold", flexGrow = 1 }),
ui.button({
glyph = showAddForm and "minus" or "plus",
variant = showAddForm and "primary" or "ghost",
controlSize = "sm",
tooltip = showAddForm and noctalia.tr("panel.tooltip_close_form") or noctalia.tr("panel.tooltip_add_host"),
onClick = function()
showAddForm = not showAddForm
if not showAddForm then
draftIp = ""
draftLabel = ""
draftError = ""
draftKey += 1
end
render()
end,
}),
ui.button({
glyph = "refresh",
variant = "ghost",
controlSize = "sm",
tooltip = noctalia.tr("panel.tooltip_check_now"),
onClick = function() noctalia.state.set("link_ip_monitor.cmd", { op = "refresh" }) end,
}),
ui.button({
glyph = "close",
variant = "ghost",
controlSize = "sm",
onClick = function() panel.close() end,
}),
}))
if showAddForm then
table.insert(column, ui.row({
gap = 6,
align = "center",
padding = 8,
radius = 8,
fill = "surface_variant/0.35",
}, {
ui.column({ flexGrow = 1, gap = 6 }, {
ui.input({
key = "add-ip-" .. draftKey,
value = draftIp,
placeholder = noctalia.tr("panel.add_placeholder"),
onChange = function(value) draftIp = value; draftError = "" end,
onSubmit = "onAdd",
}),
ui.input({
key = "add-label-" .. draftKey,
value = draftLabel,
placeholder = noctalia.tr("panel.add_label_placeholder"),
onChange = function(value) draftLabel = value end,
onSubmit = "onAdd",
}),
(draftError ~= "" and ui.label({ text = draftError, color = "#e05252", fontSize = 11 }) or nil),
}),
ui.button({ glyph = "plus", variant = "primary", controlSize = "sm", tooltip = noctalia.tr("panel.tooltip_add"), onClick = "onAdd" }),
}))
end
table.insert(column, body)
panel.render(ui.column({ gap = 8, padding = 8, flexGrow = 1 }, column))
end
function onAdd()
local host = normalizeHost(draftIp)
if noctalia.string.trim(draftIp) == "" then
return
end
if host == nil then
draftError = noctalia.tr("panel.invalid_format")
showAddForm = true
render()
return
end
local label = noctalia.string.trim(draftLabel)
draftIp = ""
draftLabel = ""
draftKey += 1
draftError = ""
showAddForm = false
noctalia.state.set("link_ip_monitor.cmd", { op = "add", ip = host, label = label })
render()
end
function onOpen(_context)
ips = noctalia.state.get("link_ip_monitor.statuses") or {}
pendingDelete = nil
draftIp = ""
draftLabel = ""
draftError = ""
draftKey += 1
showAddForm = false
render()
end
noctalia.state.watch("link_ip_monitor.statuses", function(value)
ips = (type(value) == "table") and value or {}
if pendingDelete ~= nil then
local stillThere = false
for _, item in ipairs(ips) do
if item.ip == pendingDelete then
stillThere = true
break
end
end
if not stillThere then
pendingDelete = nil
end
end
render()
end)