feat: add error handling and toast notifications (#8)
- Add errorMessage() utility for user-friendly toast messages - Show toast on tailscale up/down/set/status failures - Detect missing tailscale binary (exit code 127) - Guard toggleTailscale and setExitNode when binary unavailable - Show 'Tailscale not available' in popout when binary missing
This commit is contained in:
parent
c9d1baf7c4
commit
6129b09943
3 changed files with 67 additions and 3 deletions
|
|
@ -11,6 +11,7 @@ PluginComponent {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property bool isConnected: false
|
property bool isConnected: false
|
||||||
|
property bool binaryAvailable: true
|
||||||
property string tailscaleIP: ""
|
property string tailscaleIP: ""
|
||||||
property string currentExitNode: ""
|
property string currentExitNode: ""
|
||||||
property var peers: []
|
property var peers: []
|
||||||
|
|
@ -35,6 +36,10 @@ PluginComponent {
|
||||||
id: toggleProcess
|
id: toggleProcess
|
||||||
|
|
||||||
onExited: (code, status) => {
|
onExited: (code, status) => {
|
||||||
|
if (code !== 0) {
|
||||||
|
var action = root.isConnected ? "disconnect" : "connect"
|
||||||
|
ToastService.showError("tailscalectl", TailscaleLib.errorMessage(action))
|
||||||
|
}
|
||||||
statusCheck.running = true
|
statusCheck.running = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -48,7 +53,7 @@ PluginComponent {
|
||||||
|
|
||||||
onExited: (code, status) => {
|
onExited: (code, status) => {
|
||||||
if (code !== 0) {
|
if (code !== 0) {
|
||||||
ToastService.showError("tailscalectl", "Failed to set exit node")
|
ToastService.showError("tailscalectl", TailscaleLib.errorMessage("set"))
|
||||||
}
|
}
|
||||||
statusCheck.running = true
|
statusCheck.running = true
|
||||||
}
|
}
|
||||||
|
|
@ -99,11 +104,21 @@ PluginComponent {
|
||||||
root.tailscaleIP = ""
|
root.tailscaleIP = ""
|
||||||
root.currentExitNode = ""
|
root.currentExitNode = ""
|
||||||
root.peers = []
|
root.peers = []
|
||||||
|
if (code === 127) {
|
||||||
|
root.binaryAvailable = false
|
||||||
|
ToastService.showError("tailscalectl", "Tailscale binary not found")
|
||||||
|
} else {
|
||||||
|
ToastService.showError("tailscalectl", TailscaleLib.errorMessage("status"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleTailscale() {
|
function toggleTailscale() {
|
||||||
|
if (!root.binaryAvailable) {
|
||||||
|
ToastService.showError("tailscalectl", "Tailscale not available")
|
||||||
|
return
|
||||||
|
}
|
||||||
if (root.isConnected) {
|
if (root.isConnected) {
|
||||||
toggleProcess.command = ["tailscale", "down"]
|
toggleProcess.command = ["tailscale", "down"]
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -113,6 +128,10 @@ PluginComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setExitNode(hostname) {
|
function setExitNode(hostname) {
|
||||||
|
if (!root.binaryAvailable) {
|
||||||
|
ToastService.showError("tailscalectl", "Tailscale not available")
|
||||||
|
return
|
||||||
|
}
|
||||||
exitNodeProcess.command = TailscaleLib.makeExitNodeCommand(hostname)
|
exitNodeProcess.command = TailscaleLib.makeExitNodeCommand(hostname)
|
||||||
exitNodeProcess.running = true
|
exitNodeProcess.running = true
|
||||||
}
|
}
|
||||||
|
|
@ -138,6 +157,14 @@ PluginComponent {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: Theme.spacingM + statusRow.implicitHeight + Theme.spacingM + peerList.height + Theme.spacingM
|
height: Theme.spacingM + statusRow.implicitHeight + Theme.spacingM + peerList.height + Theme.spacingM
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
visible: !root.binaryAvailable
|
||||||
|
text: "Tailscale not available"
|
||||||
|
anchors.centerIn: parent
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: statusRow
|
id: statusRow
|
||||||
y: Theme.spacingM
|
y: Theme.spacingM
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,19 @@ function makeExitNodeCommand(hostname) {
|
||||||
return ["tailscale", "set", "--exit-node=" + hostname]
|
return ["tailscale", "set", "--exit-node=" + hostname]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function errorMessage(cmd) {
|
||||||
|
var messages = {
|
||||||
|
"up": "Failed to connect to Tailscale",
|
||||||
|
"connect": "Failed to connect to Tailscale",
|
||||||
|
"down": "Failed to disconnect from Tailscale",
|
||||||
|
"disconnect": "Failed to disconnect from Tailscale",
|
||||||
|
"set": "Failed to set exit node",
|
||||||
|
"status": "Failed to read Tailscale status"
|
||||||
|
}
|
||||||
|
return messages[cmd] || "Tailscale command failed"
|
||||||
|
}
|
||||||
|
|
||||||
// CommonJS export for Node.js tests (ignored by QML)
|
// CommonJS export for Node.js tests (ignored by QML)
|
||||||
if (typeof module !== "undefined" && module.exports) {
|
if (typeof module !== "undefined" && module.exports) {
|
||||||
module.exports = { parsePeers, makeExitNodeCommand }
|
module.exports = { parsePeers, makeExitNodeCommand, errorMessage }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { test } from "node:test"
|
import { test } from "node:test"
|
||||||
import assert from "node:assert"
|
import assert from "node:assert"
|
||||||
import lib from "../tailscalectl/lib.js"
|
import lib from "../tailscalectl/lib.js"
|
||||||
const { parsePeers, makeExitNodeCommand } = lib
|
const { parsePeers, makeExitNodeCommand, errorMessage } = lib
|
||||||
|
|
||||||
test("parsePeers extracts exitNode from ExitNodeOption", () => {
|
test("parsePeers extracts exitNode from ExitNodeOption", () => {
|
||||||
const peerMap = {
|
const peerMap = {
|
||||||
|
|
@ -34,3 +34,28 @@ test("makeExitNodeCommand with empty string clears exit node", () => {
|
||||||
const cmd = makeExitNodeCommand("")
|
const cmd = makeExitNodeCommand("")
|
||||||
assert.deepStrictEqual(cmd, ["tailscale", "set", "--exit-node="])
|
assert.deepStrictEqual(cmd, ["tailscale", "set", "--exit-node="])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("errorMessage returns user-friendly message for tailscale up failure", () => {
|
||||||
|
const msg = errorMessage("up", 1)
|
||||||
|
assert.strictEqual(msg, "Failed to connect to Tailscale")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("errorMessage returns user-friendly message for tailscale down failure", () => {
|
||||||
|
const msg = errorMessage("down", 1)
|
||||||
|
assert.strictEqual(msg, "Failed to disconnect from Tailscale")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("errorMessage returns user-friendly message for tailscale set failure", () => {
|
||||||
|
const msg = errorMessage("set", 1)
|
||||||
|
assert.strictEqual(msg, "Failed to set exit node")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("errorMessage returns user-friendly message for tailscale status failure", () => {
|
||||||
|
const msg = errorMessage("status", 1)
|
||||||
|
assert.strictEqual(msg, "Failed to read Tailscale status")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("errorMessage returns generic message for unknown command", () => {
|
||||||
|
const msg = errorMessage("unknown", 1)
|
||||||
|
assert.strictEqual(msg, "Tailscale command failed")
|
||||||
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue