* 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
360 lines
10 KiB
Luau
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)
|